Error boundaries are product surfaces
The fallback users see during a frontend failure should preserve context, protect work, explain recovery, and give engineering evidence.
A frontend error boundary is not just an engineering safety net. It is a product surface that appears at the exact moment the user's trust is weakest.
That is why the default "something went wrong" screen bothers me. It may be technically honest, but it is usually not useful. It does not tell the user what broke, whether their work is safe, what they can still do, or whether retrying will make things worse. It turns a recoverable product moment into a dead end.
I think error boundaries deserve design attention for the same reason empty states and loading states do. They are not edge cases. They are part of the product's behavior under stress.
The goal is not to make errors cute. The goal is to help the user recover without lying.
Boundary size should match consequence: widget, panel, route, or whole app.
Retry, refresh, go back, copy work, contact support, or continue elsewhere.
Error IDs, route, user action, release version, and component context.
Choose the boundary size carefully
The easiest boundary is the whole app. Wrap everything. Catch everything. Show one fallback.
That is rarely the best user experience.
If a chart fails, the user may still be able to read the table. If a sidebar widget fails, the main workflow may still be fine. If a settings subsection fails, the user may still save another section. A whole-app fallback turns a local failure into a total product failure.
I think about boundary scope in layers:
- Component boundary: for isolated widgets, charts, previews, and optional modules.
- Panel boundary: for drawers, side panels, and dashboard cards.
- Route boundary: for pages where the main data contract failed.
- App boundary: for shell-level failures where the product cannot safely continue.
The more local the boundary, the more useful the product can remain. But local boundaries need design. They cannot all say the same generic thing. A failed chart, failed upload preview, and failed permissions panel need different recovery paths.
Preserve nearby context
When something breaks, users orient themselves by what remains.
A good error boundary preserves surrounding context when it can. The page title stays. The object name stays. Navigation stays. The last known data may stay if it is safe. The user should not feel thrown out of the product unless the failure really requires it.
For example, if an order timeline fails to render, the order page can still show customer name, order number, total, and primary actions. The timeline card can explain that activity could not load and offer retry. That is much better than replacing the whole route with a blank error page.
Preserving context also helps support. A user can say "the activity panel on order 1042 failed" instead of "the app broke."
Make retry specific
Retry is often the only action on an error screen. That makes it important.
A retry button should retry the thing that failed, not reload the world unless that is truly necessary. If the failed object is a chart query, retry the chart query. If the failed object is a route data load, retry the route data load. If the app shell is corrupted, then a full refresh may be the right move.
The copy should match the scope:
- "Retry chart"
- "Reload customer"
- "Try importing again"
- "Refresh this page"
- "Return to dashboard"
Specific retry copy makes the product feel more controlled. It tells the user the team understands the failure boundary.
One broken widget forces the user out of the workflow.
The failing area owns the recovery action.
When safe, preserve stale context and explain freshness.
Protect user work first
Error boundaries in forms and editors need extra care. If the user has typed something, configured a rule, uploaded a file, or composed a message, the fallback has one job before anything else: protect the work.
That may mean:
- keeping draft state outside the crashing component
- saving local drafts before risky previews render
- letting the user copy raw input
- disabling only the broken preview
- restoring the last stable editor state
- clearly saying whether changes were saved
The worst error boundary is one that loses work and says nothing.
For product teams, this means state architecture matters. If all form state lives inside a component that can crash, the fallback may not have access to the thing the user cares about. Designing recovery sometimes requires moving state up, storing drafts locally, or separating preview rendering from input.
This is where frontend architecture and UX become the same conversation.
Do not over-apologize
Error copy should be calm, specific, and useful.
I avoid copy like:
- "Oops!"
- "Uh oh!"
- "Something went wrong."
- "Our bad."
Sometimes friendly copy is fine, but it rarely helps under stress. I prefer:
- "The activity timeline could not load."
- "Your draft is safe, but the preview failed."
- "We could not update permissions. Nothing changed."
- "This chart failed to render. The rest of the dashboard is still available."
The best error copy answers three questions:
- What failed?
- What happened to my work?
- What can I do next?
If the product can answer those questions, the tone can stay simple.
Give engineering a useful trail
A user-facing error boundary should also create useful evidence.
At minimum, I want the error report to include:
- route
- component or boundary name
- release version
- user action if known
- object ID if relevant
- browser and device context
- error ID shown to the user
The visible error ID matters because it connects user reports to logs. If support receives "I saw error 8K4D on the billing page," engineering can find the event quickly. Without that bridge, debugging becomes screenshot archaeology.
The trick is to keep the user-facing surface clean while sending rich context behind the scenes.
Clear copy, scoped retry, and reassurance about work.
Enough detail to route the issue without guessing.
The report points to the failing surface and deployed version.
Design stale-data fallbacks
Not every failure has to show an empty error state. Sometimes the most useful fallback is stale data with a clear freshness label.
For example:
- "Showing data from 8:42 AM. Refresh failed."
- "Latest sync failed. Last successful import was yesterday."
- "Preview could not update. Your saved settings are unchanged."
Stale data can be dangerous if it looks fresh, so the label matters. But when done honestly, it keeps the product usable. An operator may still make progress with slightly old information. A store owner may still understand the trend. A support teammate may still answer the customer.
The product should decide where stale data is acceptable. Financial totals, permissions, inventory, and customer-facing messages may need stricter behavior. Dashboards and read-only summaries may tolerate stale context better.
Again, this is a product decision, not just an engineering fallback.
Make accessibility part of the fallback
Error fallbacks are often built late, which is how accessibility gets skipped.
A fallback still needs:
- a useful heading
- focus management when it replaces interactive content
- keyboard reachable actions
- readable contrast
- text that does not depend only on color
- status announcements when content fails after load
If a panel fails after the user activates a control, focus should not disappear. If a route-level boundary replaces the main content, the user should land somewhere meaningful. If the error appears asynchronously, assistive technology may need a polite announcement.
This is not extra polish. A broken state that is also inaccessible compounds the failure.
Avoid infinite retry loops
Retry needs limits.
If the same boundary fails repeatedly, the product should change strategy. Maybe the second failure offers a full refresh. Maybe the third failure suggests contacting support with an error ID. Maybe the product disables a risky preview and preserves the rest of the form.
An infinite retry button teaches the user nothing. It turns recovery into superstition.
I like tracking retry count in the boundary state. The UI can stay simple:
- first failure: retry locally
- second failure: refresh page or keep stale data
- third failure: show support path with error ID
The exact thresholds depend on the product, but the principle matters. A product should notice when its recovery path is not recovering.
Use boundaries as architecture feedback
Error boundaries reveal architecture quality.
If one optional widget crashing takes down the whole route, the route may be too tightly coupled. If a preview crash destroys form input, state may live in the wrong place. If every error needs a full refresh, data fetching may not be scoped clearly. If support cannot debug an error ID, observability may be too thin.
I like treating boundary work as feedback for the codebase. The fallback is the user-facing part. The underlying lesson may be that the product needs clearer state ownership, smaller components, safer data adapters, or more honest loading boundaries.
That is why I do not think of error boundaries as a final coat of paint. They are pressure tests for product architecture.
Include failure in design review
I do not like reviewing only the successful screen for surfaces that can lose money, permissions, or saved work.
When a design review covers a meaningful product surface, I want at least one failure pass in the same review. The team should see what happens if the primary query fails, if a mutation fails after the user clicks save, if a third-party preview crashes, and if a retry fails twice. Those states do not need as many pixels as the happy path, but they need enough attention that engineering is not inventing the product response alone.
This is especially important for AI-assisted UI work. Generated screens often look plausible in the ready state and fall apart in error states. The layout collapses, the copy gets generic, and the controls do not match the failed action. A boundary review catches that because it asks the screen to behave when it is no longer showing the ideal demo.
I also like pairing design review with an implementation note. Which boundary owns this fallback? Which error ID is shown? Which draft state survives? Which logs receive the release version? That short note turns the failure state from a mockup into an engineering contract.
The review gets better when the team names the surface's promise out loud. A billing form promises that money-related changes are either saved or not saved, never ambiguous. A dashboard promises that stale information is labeled. A destructive-action modal promises that failure leaves the object intact. Once the promise is explicit, the boundary copy and retry action have a standard to meet.
That is the part I want to see more often in product work. Not just "we handled the exception," but "we protected the promise of this surface when the exception happened."
Design examples by surface
Different surfaces need different fallback behavior.
For a dashboard card, I may show the card title, stale data if available, a retry action, and a small explanation. The rest of the dashboard stays usable.
For a settings form, I care more about preserving input. If a preview fails, the editable fields should remain. If save fails, the product should say whether the current values are local drafts or persisted settings.
For a destructive confirmation modal, a failure should be conservative. If the delete request fails, the object should remain visible and the product should clearly say nothing was deleted.
For an editor, the fallback should prioritize draft recovery. A broken formatting preview should not destroy the text. A failed autosave should make save status obvious.
The boundary copy and controls should reflect the surface. This is why one generic fallback component rarely works everywhere. A shared visual style is fine. The product behavior has to be specific.
Keep fallback layout stable
Errors can create layout shifts that make the product feel even more broken.
If a chart fails, replacing a 360px card with a 90px message can cause the rest of the dashboard to jump. If a table error collapses the table area, pagination and filters may move. If a route fallback removes the sidebar, the user loses navigation.
I prefer fallbacks that preserve the approximate footprint of the failed surface. The card stays a card. The panel stays a panel. The route keeps its shell. This makes the failure feel contained.
Stable layout also helps with retry. The user's pointer and keyboard context remain near the problem instead of being displaced by the fallback.
Make failure copy reusable without becoming generic
Teams often swing between two bad options: every error message is custom, or every error message says the same vague thing.
The better pattern is reusable structure with specific nouns:
- "[Surface] could not load."
- "Your [work] is safe."
- "Try [specific retry action]."
- "If this keeps happening, share error [id]."
This gives the product consistency without erasing context. It also helps engineers implement good copy quickly because they are not inventing error language from scratch every time.
The design system can own this pattern. Not by forcing one message, but by giving teams a grammar for recovery.
Test boundaries intentionally
I like adding a QA path for error boundaries, especially in product surfaces with money, permissions, or saved work.
The test plan should include:
- Throw inside an optional widget.
- Fail a route-level data request.
- Fail a mutation after user input.
- Fail a preview renderer while preserving input.
- Fail on mobile.
- Fail with long object names.
- Confirm the error report includes the boundary name.
If the team cannot trigger the fallback intentionally, it probably has not really designed the fallback.
This does not require a complex chaos testing setup for every small feature. It can be a mocked throw, a local flag, a Storybook state, or a test route. The important thing is seeing the fallback before users do.
My boundary checklist
Before I trust an error boundary, I want:
- scoped failure instead of whole-app failure where possible
- preserved context
- specific copy
- local retry
- user work protection
- error ID or support bridge
- engineering context in logs
- stale-data behavior when appropriate
- QA path to trigger the fallback
The craft is not in pretending errors will not happen. The craft is in deciding what the product does when they do.
An error boundary is a promise: even when part of the interface fails, the product will stay honest, preserve what it can, and help the user take the next reasonable step.
Use this after reading.
Practical downloads and templates that turn the article into something you can bring into a product review, implementation pass, or agent workflow.
Front-End State Recipes
Reusable recipes for optimistic actions, loading, empty, error, data-transition, and disabled-control states.
UI PR Risk Review Checklist
A merge-readiness checklist for product intent, states, accessibility, visual durability, and UI implementation risk.