C2 — Error-to-UI Matrix
Scope: Every canonical error code in
docs/standards/ERROR_CODES.mdmapped to its frontend UI treatment. For each code: which UI pattern to use, whether to show retry, whether to escalate to an error page vs. inline toast vs. dialog, and which surface(s) it applies to.How to use: When a BFF call returns an
error.codefield, look up that code below and render the mapped UI. All user-visible strings are resolved through the i18n bundle using theuserMessageKeyfromERROR_CODES.md; do not hard-code English strings.Pattern definitions: see C3 — Empty/Loading/Error State Catalog.
1. UI pattern taxonomy
| Pattern | When to use | User can retry? | Dismissible? |
|---|---|---|---|
| Toast / snackbar | Transient recoverable errors; non-blocking | Yes (auto-retry or CTA) | Yes (auto-dismiss 5 s) |
| Inline field error | Form validation failure on a specific field | N/A | On correction |
| Inline banner | Section-level warning that does not block the whole screen | Sometimes | Yes |
| Full-screen error | Unrecoverable fatal, auth failure, tenant suspended | No / manual action | No |
| Modal dialog | Confirmation required before retrying a destructive action | Yes after confirm | Yes |
| Offline indicator | Network unavailable; queued for retry | Auto (when online) | No |
| Skeleton + retry | Fetching failed on a card/panel; data still loadable | Yes (manual) | — |
| Silent retry | retriable: true, no user action required; show spinner | Auto | — |
2. Error-to-UI mapping
2.1 GENERAL
| Error code | HTTP | UI pattern | User message | Retry CTA | Escalation | Surface(s) |
|---|---|---|---|---|---|---|
MELMASTOON.GENERAL.RESOURCE_NOT_FOUND | 404 | Full-screen error or skeleton + retry depending on context | errors.general.resource_not_found | "Go home" | — | all |
MELMASTOON.GENERAL.VALIDATION_FAILED | 422 | Inline field errors (per errors[] array) | errors.general.validation_failed | — | — | all |
MELMASTOON.GENERAL.PRECONDITION_FAILED | 412 | Toast with "Reload" CTA | errors.general.precondition_failed | "Reload" | — | all |
MELMASTOON.GENERAL.CROSS_TENANT_REFERENCE | 422 | Toast (technical; phrased as "Something went wrong") | errors.general.cross_tenant_reference | — | Log alert | operator-desktop |
MELMASTOON.GENERAL.RATE_LIMITED | 429 | Toast with countdown timer; auto-retry after retryAfter | errors.general.rate_limited | Auto | — | all |
MELMASTOON.GENERAL.INTERNAL | 500 | Toast "Something went wrong" + trace ID (collapsed) | errors.general.internal | "Try again" | Sentry alert | all |
2.2 IDENTITY
| Error code | HTTP | UI pattern | User message | Retry CTA | Escalation | Surface(s) |
|---|---|---|---|---|---|---|
MELMASTOON.IDENTITY.INVALID_CREDENTIALS | 401 | Inline field error on login form | errors.identity.invalid_credentials | — | Lock after 5 attempts | all |
MELMASTOON.IDENTITY.TOKEN_EXPIRED | 401 | Silent token refresh → if fails, modal "Session expired, please log in" | errors.identity.token_expired | Auto refresh → re-login modal | — | all |
MELMASTOON.IDENTITY.MFA_REQUIRED | 401 | Step-up MFA modal | errors.identity.mfa_required | — | — | operator-desktop, control-plane-web |
MELMASTOON.IDENTITY.DEVICE_NOT_BOUND | 403 | Full-screen error with device pairing instructions | errors.identity.device_not_bound | "Pair device" | — | operator-desktop |
MELMASTOON.IDENTITY.PERMISSION_DENIED | 403 | Inline banner "You do not have permission to do this" | errors.identity.permission_denied | — | — | all |
MELMASTOON.IDENTITY.SESSION_REVOKED | 401 | Full-screen error, force logout | errors.identity.session_revoked | "Log in again" | — | all |
2.3 TENANT
| Error code | HTTP | UI pattern | User message | Retry CTA | Escalation | Surface(s) |
|---|---|---|---|---|---|---|
MELMASTOON.TENANT.NOT_FOUND | 404 | Full-screen error "Property not found" | errors.tenant.not_found | — | — | tenant-booking-web, consumer-mobile |
MELMASTOON.TENANT.SUSPENDED | 403 | Full-screen error "This property is temporarily unavailable" | errors.tenant.suspended | — | Notify GM if in operator-desktop | all |
MELMASTOON.TENANT.NOT_A_MEMBER | 403 | Full-screen error "You don't have access to this property" | errors.tenant.not_a_member | — | — | operator-desktop |
MELMASTOON.TENANT.PLAN_LIMIT_EXCEEDED | 402 | Modal "Plan limit reached — upgrade your plan" | errors.tenant.plan_limit_exceeded | "Upgrade" | — | operator-desktop, control-plane-web |
MELMASTOON.TENANT.CONFIGURATION_INVALID | 422 | Inline banner in configuration panel | errors.tenant.configuration_invalid | Fix fields → save | — | control-plane-web, operator-desktop |
2.4 RESERVATION
| Error code | HTTP | UI pattern | User message | Retry CTA | Escalation | Surface(s) |
|---|---|---|---|---|---|---|
MELMASTOON.RESERVATION.OVERBOOKING_BLOCKED | 409 | Full-screen error within booking funnel "This room is no longer available — another guest just booked it" | errors.reservation.overbooking_blocked | "Find another room" | — | tenant-booking-web, consumer-mobile |
MELMASTOON.RESERVATION.HOLD_EXPIRED | 410 | Modal "Your hold expired — please start over" + countdown in timer | errors.reservation.hold_expired | "Start over" | — | tenant-booking-web, consumer-mobile |
MELMASTOON.RESERVATION.INVALID_STATE_TRANSITION | 409 | Toast "This action is not allowed at this stage" | errors.reservation.invalid_state_transition | — | — | operator-desktop |
MELMASTOON.RESERVATION.GUEST_INFO_INCOMPLETE | 422 | Inline field errors on guest capture form | errors.reservation.guest_info_incomplete | — | — | operator-desktop, tenant-booking-web |
MELMASTOON.RESERVATION.CHECKIN_WINDOW_VIOLATED | 409 | Inline banner "Check-in is only available between HH:MM and HH:MM; request an override?" | errors.reservation.checkin_window_violated | "Request override" | — | operator-desktop |
MELMASTOON.RESERVATION.CANCELLATION_NOT_ALLOWED | 409 | Modal "This booking cannot be cancelled under the non-refundable policy" | errors.reservation.cancellation_not_allowed | — | — | operator-desktop, tenant-booking-web |
2.5 INVENTORY
| Error code | HTTP | UI pattern | User message | Retry CTA | Escalation | Surface(s) |
|---|---|---|---|---|---|---|
MELMASTOON.INVENTORY.INSUFFICIENT_AVAILABILITY | 409 | Funnel error "No rooms available for these dates" | errors.inventory.insufficient_availability | "Change dates" | — | tenant-booking-web, consumer-mobile |
MELMASTOON.INVENTORY.STOP_SELL_ACTIVE | 409 | Inline banner on calendar "These dates are closed" | errors.inventory.stop_sell_active | "Change dates" | — | tenant-booking-web, operator-desktop |
MELMASTOON.INVENTORY.MIN_LOS_VIOLATED | 422 | Inline banner "Minimum stay is N nights" | errors.inventory.min_los_violated | "Adjust dates" | — | tenant-booking-web, consumer-mobile |
MELMASTOON.INVENTORY.MAX_LOS_VIOLATED | 422 | Inline banner "Maximum stay is N nights" | errors.inventory.max_los_violated | "Adjust dates" | — | tenant-booking-web |
MELMASTOON.INVENTORY.OVERSELL_DETECTED | 500 | Toast "Something went wrong — our team has been alerted" | errors.general.internal | — | P0 alert to on-call | operator-desktop |
2.6 PRICING
| Error code | HTTP | UI pattern | User message | Retry CTA | Escalation | Surface(s) |
|---|---|---|---|---|---|---|
MELMASTOON.PRICING.RATE_PLAN_NOT_FOUND | 404 | Skeleton + retry on rate card | errors.pricing.rate_plan_not_found | "Reload" | — | tenant-booking-web |
MELMASTOON.PRICING.QUOTE_EXPIRED | 410 | Modal "Prices may have changed — your quote has expired" | errors.pricing.quote_expired | "Re-quote" | — | tenant-booking-web, consumer-mobile |
MELMASTOON.PRICING.CURRENCY_MISMATCH | 422 | Toast "Currency mismatch — refreshing…" + auto re-quote | errors.pricing.currency_mismatch | Auto | — | tenant-booking-web |
MELMASTOON.PRICING.DERIVATION_FAILED | 500 | Silent retry (retriable) → after 3 attempts, toast | errors.pricing.derivation_failed | Auto → "Try again" | — | tenant-booking-web |
2.7 BILLING
| Error code | HTTP | UI pattern | User message | Retry CTA | Escalation | Surface(s) |
|---|---|---|---|---|---|---|
MELMASTOON.BILLING.FOLIO_LOCKED | 409 | Modal "This folio is closed — a supervisor can reopen it" | errors.billing.folio_locked | "Request reopen" | — | operator-desktop |
MELMASTOON.BILLING.CHARGE_INVALID | 422 | Inline field errors on charge form | errors.billing.charge_invalid | — | — | operator-desktop |
MELMASTOON.BILLING.REFUND_EXCEEDS_BALANCE | 422 | Inline field error "Refund cannot exceed remaining balance of AFN X" | errors.billing.refund_exceeds_balance | — | — | operator-desktop |
MELMASTOON.BILLING.RECONCILIATION_MISMATCH | 500 | Full-screen error on EOD panel + finance on-call alert | errors.general.internal | — | P0 finance alert | operator-desktop |
MELMASTOON.BILLING.TAX_RULE_MISSING | 422 | Toast "Tax rule not configured for this charge — contact your admin" | errors.billing.tax_rule_missing | — | — | operator-desktop |
2.8 PAYMENT
| Error code | HTTP | UI pattern | User message | Retry CTA | Escalation | Surface(s) |
|---|---|---|---|---|---|---|
MELMASTOON.PAYMENT.DECLINED | 402 | Inline error on payment step "Your card was declined — try a different card or pay on arrival" | errors.payment.declined | "Try again / Switch method" | — | tenant-booking-web, consumer-mobile |
MELMASTOON.PAYMENT.INSUFFICIENT_FUNDS | 402 | Same as DECLINED, distinct copy | errors.payment.insufficient_funds | "Switch method" | — | tenant-booking-web, consumer-mobile |
MELMASTOON.PAYMENT.GATEWAY_TIMEOUT | 504 | Toast "Payment service is slow — retrying…" → auto-retry 2x → "Please try again later" | errors.payment.gateway_timeout | Auto 2x → manual | — | tenant-booking-web, consumer-mobile |
MELMASTOON.PAYMENT.INTENT_NOT_FOUND | 404 | Modal "Your payment session expired — start over" | errors.payment.intent_not_found | "Start over" | — | tenant-booking-web |
MELMASTOON.PAYMENT.CASH_RECONCILIATION_PENDING | 202 | Not an error — informational toast "Cash payment pending reconciliation by front desk" | errors.payment.cash_reconciliation_pending | — | — | operator-desktop |
2.9 LOCK
| Error code | HTTP | UI pattern | User message | Retry CTA | Escalation | Surface(s) |
|---|---|---|---|---|---|---|
MELMASTOON.LOCK.VENDOR_UNREACHABLE | 502 | Modal "Key system is unavailable — fall back to mechanical key?" | errors.lock.vendor_unreachable | "Use mechanical key" | — | operator-desktop, kiosk |
MELMASTOON.LOCK.KEY_ISSUE_FAILED | 502 | Modal with retry + mechanical key fallback | errors.lock.key_issue_failed | "Retry / Use mechanical key" | — | operator-desktop, kiosk |
MELMASTOON.LOCK.KEY_REVOKE_FAILED | 502 | Toast "Key could not be revoked — alert maintenance" | errors.lock.key_revoke_failed | "Retry" | Security alert | operator-desktop |
MELMASTOON.LOCK.CARD_ENCODER_OFFLINE | 503 | Inline banner "Card encoder offline — encode when reconnected" | errors.lock.card_encoder_offline | Auto on reconnect | — | operator-desktop |
MELMASTOON.LOCK.DEVICE_NOT_PAIRED | 409 | Inline banner with "Pair lock device" link | errors.lock.device_not_paired | "Pair device" | — | operator-desktop |
MELMASTOON.LOCK.CREDENTIAL_EXPIRED | 410 | Toast "Key expired — re-issue required" | errors.lock.credential_expired | "Re-issue key" | — | operator-desktop |
2.10 HOUSEKEEPING
| Error code | HTTP | UI pattern | User message | Retry CTA | Escalation | Surface(s) |
|---|---|---|---|---|---|---|
MELMASTOON.HOUSEKEEPING.ROOM_STATE_CONFLICT | 409 | Toast "Room status changed — refresh your view" | errors.housekeeping.room_state_conflict | "Refresh" | — | operator-desktop, kiosk, staff-mobile |
MELMASTOON.HOUSEKEEPING.TASK_ALREADY_COMPLETED | 409 | Toast "This task was already completed" | errors.housekeeping.task_already_completed | — | — | operator-desktop, kiosk, staff-mobile |
MELMASTOON.HOUSEKEEPING.STAFF_UNAVAILABLE | 409 | Inline banner on assignment panel | errors.housekeeping.staff_unavailable | "Assign to another" | — | operator-desktop |
2.11 SYNC (desktop)
| Error code | HTTP | UI pattern | User message | Retry CTA | Escalation | Surface(s) |
|---|---|---|---|---|---|---|
MELMASTOON.SYNC.CONFLICT_UNRESOLVABLE | 409 | Modal with side-by-side conflict resolution UI | errors.sync.conflict_unresolvable | Manual pick | — | operator-desktop |
MELMASTOON.SYNC.SEQUENCE_GAP | 409 | Inline banner "Sync incomplete — some events may be missing" | errors.sync.sequence_gap | "Force full sync" | — | operator-desktop |
MELMASTOON.SYNC.PUSH_REJECTED | 422 | Toast "A change couldn't be saved — check sync log" | errors.sync.push_rejected | — | — | operator-desktop |
MELMASTOON.SYNC.STATE_CORRUPT | 500 | Full-screen error "Local data is corrupted — reset required" + backup prompt | errors.sync.state_corrupt | "Reset + resync" | P1 Sentry alert | operator-desktop |
2.12 THEME
| Error code | HTTP | UI pattern | User message | Retry CTA | Escalation | Surface(s) |
|---|---|---|---|---|---|---|
MELMASTOON.THEME.CONTRAST_INVARIANT_VIOLATED | 422 | Inline error on token picker "This color combination fails WCAG AA contrast" | errors.theme.contrast_invariant_violated | Fix token | — | control-plane-web |
MELMASTOON.THEME.RTL_VARIANT_MISSING | 422 | Inline error on token form | errors.theme.rtl_variant_missing | Add RTL variant | — | control-plane-web |
MELMASTOON.THEME.PUBLISH_IN_PROGRESS | 409 | Toast "A publish is already in progress — try again shortly" | errors.theme.publish_in_progress | Auto poll → retry | — | control-plane-web |
3. Offline error handling (desktop)
When the Electron desktop is offline:
- Do not surface BFF error codes — the request never reached the BFF.
- Show the Offline indicator pattern (sticky banner or status icon in nav).
- All mutations go to the local outbox; confirm to the user via "Saved locally — will sync when reconnected".
- Reads fall through to local SQLite; show a "data as of <last sync time>" label.
- After reconnect, flush outbox. Surface any
SYNC.*errors from the flush.
4. Error UX rules
- Never show raw error codes to end users; show the
userMessageKeystring only. - Always show a trace ID (collapsed toggle) on 5xx errors so support can correlate.
- Never auto-dismiss errors that require user action (payment failed, folio locked, etc.).
- Toast auto-dismiss is 5 seconds for non-critical, 8 seconds for recoverable errors with retry.
- Retry limits: automatic retries cap at 3 attempts with exponential backoff (200 ms, 400 ms, 800 ms). After cap, show manual retry CTA.
- Accessibility: all error states must be announced via
role="alert"oraria-live="assertive". Do not rely on color alone.
5. Open Questions
- Should we add
userMessageKeyShortfor toast-specific shorter copy (current key strings may be too long for 1-line toasts)? - Should
SYNC.CONFLICT_UNRESOLVABLEtrigger a push notification to the GM if unresolved for > 5 minutes? - How should kiosk handle
LOCK.KEY_ISSUE_FAILED? The kiosk has no "call front desk" affordance — add an in-kiosk staff-paging button?
References
../../standards/ERROR_CODES.md— canonical error code definitionsC3-empty-loading-error-state-catalog.md— UI pattern definitions../common/09-non-functional-requirements.md../journeys/README.md