CANON · Error Handling
Errors are inevitable. Showing them well is the difference between a product that feels reliable and one that feels broken.
Error message anatomy
Every error message answers three questions:
- What failed? — Name the thing. "Couldn't save your changes."
- Why? — Give the cause if known. "The server didn't respond."
- How to fix? — Give the action. "Check your connection and try again."
Bad: "Error."
Bad: "Something went wrong."
Good: "Couldn't save your changes — connection lost. Check your network and retry."
Error display patterns
| Pattern |
Use when |
| Inline (below the field) |
Field-level validation errors |
| Banner (top of page/section) |
Page-level errors affecting multiple things |
| Toast |
Non-critical background errors ("Couldn't sync — retrying") |
| Modal |
Critical errors requiring explicit acknowledgment |
| Full page (error page) |
Route-level failure (404, 500, auth expired) |
| Error boundary |
Component crash that shouldn't take down the page |
Validation timing
From canon-forms:
- Validate on blur (when user leaves the field), not on every keystroke.
- Show error after first submission attempt, then live-validate on change.
- Never show errors before the user has interacted with the field.
Retry logic
| Strategy |
Use |
| Immediate retry button |
User-triggered (network errors, form submission) |
| Auto-retry with backoff |
Background operations (sync, polling) |
| Retry with exponential backoff |
API calls: wait 1s, 2s, 4s, 8s, cap at 30s |
| No retry |
Permanent errors (404, auth revoked, validation failure) |
Show the retry state: "Retrying in 3s... [Retry now]"
Optimistic UI rollback
When using optimistic updates (show success instantly, sync in background), the error case is the rollback:
- Apply the change immediately in the UI.
- Send the request.
- On failure: revert the UI, show a toast explaining what happened.
- "Couldn't delete item — restored. [Try again]"
Error boundaries (React)
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() { return { hasError: true }; }
componentDidCatch(error, info) { logErrorToService(error, info); }
render() {
if (this.state.hasError) {
return <ErrorFallback => this.setState({ hasError: false })} />;
}
return this.props.children;
}
}
- Wrap at the feature level, not the app level (one broken widget shouldn't blank the page).
- Fallback UI should have a retry action.
- Log the error to your monitoring service.
Offline handling
- Detect via
navigator.onLine + online/offline events (unreliable alone; also check fetch failures).
- Show a persistent banner: "You're offline. Changes will sync when you're back."
- Queue offline actions, replay on reconnect.
- Never show "Something went wrong" for offline — name it.
Anti-patterns
| Anti-pattern |
Why it fails |
| "Something went wrong" with no detail |
Useless |
| "Error code: 0x80004005" |
Meaningless to users |
| "Oops!" / "Uh oh!" |
Infantilizing in a frustrating moment |
| Red border only (no text) |
Unclear what's wrong or how to fix |
| Error toast for form validation |
Wrong pattern; validation is inline |
| Auto-retry without telling the user |
Silent failure looks like hang |
| No error boundary (entire app crashes) |
One bad component blanks the screen |
| Showing stack traces to users |
Security risk + useless |
Audit checklist
Sources
- WCAG 2.2 · 3.3.1 Error Identification, 3.3.3 Error Suggestion
canon-ux-writing for message anatomy
canon-states for empty/error/loading patterns
- React docs · Error Boundaries
1---2name: canon-error-handling3description: Use when designing error states, error messages, recovery flows, fallback UIs, error boundaries, retry logic, or offline handling. Covers the three-part error message anatomy, inline vs toast vs page errors, error boundaries in React, optimistic-UI rollback, and the retry with backoff pattern. Trigger when the user mentions error, error handling, error state, fallback, retry, offline, or error boundary.4---56# CANON · Error Handling78Errors are inevitable. Showing them well is the difference between a product that feels reliable and one that feels broken.910## Error message anatomy1112Every error message answers three questions:13141. **What failed?** — Name the thing. "Couldn't save your changes."152. **Why?** — Give the cause if known. "The server didn't respond."163. **How to fix?** — Give the action. "Check your connection and try again."1718```19Bad: "Error."20Bad: "Something went wrong."21Good: "Couldn't save your changes — connection lost. Check your network and retry."22```2324## Error display patterns2526| Pattern | Use when |27|---|---|28| Inline (below the field) | Field-level validation errors |29| Banner (top of page/section) | Page-level errors affecting multiple things |30| Toast | Non-critical background errors ("Couldn't sync — retrying") |31| Modal | Critical errors requiring explicit acknowledgment |32| Full page (error page) | Route-level failure (404, 500, auth expired) |33| Error boundary | Component crash that shouldn't take down the page |3435## Validation timing3637From `canon-forms`:38- Validate on blur (when user leaves the field), not on every keystroke.39- Show error after first submission attempt, then live-validate on change.40- Never show errors before the user has interacted with the field.4142## Retry logic4344| Strategy | Use |45|---|---|46| Immediate retry button | User-triggered (network errors, form submission) |47| Auto-retry with backoff | Background operations (sync, polling) |48| Retry with exponential backoff | API calls: wait 1s, 2s, 4s, 8s, cap at 30s |49| No retry | Permanent errors (404, auth revoked, validation failure) |5051Show the retry state: "Retrying in 3s... [Retry now]"5253## Optimistic UI rollback5455When using optimistic updates (show success instantly, sync in background), the error case is the rollback:56571. Apply the change immediately in the UI.582. Send the request.593. On failure: revert the UI, show a toast explaining what happened.604. "Couldn't delete item — restored. [Try again]"6162## Error boundaries (React)6364```jsx65class ErrorBoundary extends React.Component {66 state = { hasError: false };67 static getDerivedStateFromError() { return { hasError: true }; }68 componentDidCatch(error, info) { logErrorToService(error, info); }69 render() {70 if (this.state.hasError) {71 return <ErrorFallback onRetry={() => this.setState({ hasError: false })} />;72 }73 return this.props.children;74 }75}76```7778- Wrap at the feature level, not the app level (one broken widget shouldn't blank the page).79- Fallback UI should have a retry action.80- Log the error to your monitoring service.8182## Offline handling8384- Detect via `navigator.onLine` + `online`/`offline` events (unreliable alone; also check fetch failures).85- Show a persistent banner: "You're offline. Changes will sync when you're back."86- Queue offline actions, replay on reconnect.87- Never show "Something went wrong" for offline — name it.8889## Anti-patterns9091| Anti-pattern | Why it fails |92|---|---|93| "Something went wrong" with no detail | Useless |94| "Error code: 0x80004005" | Meaningless to users |95| "Oops!" / "Uh oh!" | Infantilizing in a frustrating moment |96| Red border only (no text) | Unclear what's wrong or how to fix |97| Error toast for form validation | Wrong pattern; validation is inline |98| Auto-retry without telling the user | Silent failure looks like hang |99| No error boundary (entire app crashes) | One bad component blanks the screen |100| Showing stack traces to users | Security risk + useless |101102## Audit checklist103104- [ ] Every error message says what, why, and how105- [ ] Inline errors for fields, banners for pages, toasts for background106- [ ] Validation on blur, not on keystroke107- [ ] Retry button for recoverable errors108- [ ] Auto-retry uses exponential backoff with visible state109- [ ] Optimistic UI has a rollback + explanation110- [ ] Error boundaries at feature level (React)111- [ ] Offline state shows a banner, not a generic error112- [ ] No "Oops!", no stack traces, no bare error codes113114## Sources115116- WCAG 2.2 · 3.3.1 Error Identification, 3.3.3 Error Suggestion117- `canon-ux-writing` for message anatomy118- `canon-states` for empty/error/loading patterns119- React docs · Error Boundaries