When a data request fails, how do you decide between rendering an error branch in the component and letting the failure escalate to an ancestor handler?
answer
- blast radius first
- who still knows what to retry
- expected local, unexpected upward
- one region, one message
- distinguish offline from refused
basics
~20 sDecide by blast radius and by what can be retried. A local branch keeps the rest of the screen alive and a precise retry next to the component that issued the request. Escalate when the failure invalidates the whole region.
solid answer
~50 sAsk two questions. First, how much of the screen is invalidated: a failed widget inside an otherwise complete page should not take the page down, while a record that failed to load makes every panel describing that record meaningless, so the region should fail as a unit. Second, what the user can do next: the component that issued the request still holds its parameters, so the error branch it renders can offer a precise retry of exactly that request, whereas an ancestor handler that caught a thrown failure typically knows only that something below it broke — its honest affordance is a coarse reset of the region, not a retry. So expected, recoverable, local failures get a local branch; failures that are unexpected or that invalidate the whole region escalate. How the escalation is captured and where the handler sits is the error-boundary topic, not this decision.
go deeper
Know that a failed request needs a visible branch with a way to try again, and that failing one small part of a screen should not blank the whole screen.
Explain the two mechanisms and what each layer knows: the component still holds its request parameters, the ancestor does not. Derive the retry affordance difference from that.
Apply it to a real screen: draw the region each failure invalidates, choose the mechanism per region, and differentiate offline, refused, unauthorised and malformed so the control offered can actually help.
Decide the policy and the escalation boundaries the app ships with, including bounded retry behaviour so clients do not amplify a backend incident. Unowned, this drifts into full-page failures for trivial causes.
## Two questions decide it **1. What does the failure invalidate?** Draw the smallest region on the screen that is meaningless without this data. A recommendations strip failing invalidates a strip. The record a detail page is about failing to load invalidates every panel on that page, including the tabs and the action bar, because they all describe something that is not there. **2. What could the user do about it?** The affordance is what separates the two mechanisms in practice, and it follows from what each layer knows. | | Error branch in the component | Failure escalated to an ancestor handler | |---|---|---| | Blast radius | The one region that failed | Everything below the handler | | Rest of the screen | Keeps working | Replaced | | Context available | The exact request and its parameters | That something below it failed | | Retry affordance | Re-run this request, in place | A coarse reset of the region, or a reload | | View state around it | Preserved | Discarded with the subtree | | Good for | Expected, recoverable, local failures | Failures that make the region meaningless, and unexpected ones | ## Why the retry affordance is the real argument A component that failed still has everything needed to try again: the identifier it was fetching, the filters, the page, the sort. Its error branch can therefore render a retry that re-issues exactly one request and, on success, drops the user back where they were. An ancestor handler has none of that. It can offer to reset the region so the subtree mounts again and re-issues whatever it issues, which works, but it is a blunter instrument: it throws away sibling state, scroll position and anything the user had typed inside the region. That is acceptable when the region was worthless anyway, and wasteful when one widget was merely unlucky. ## A sensible default policy 1. **Expected failures get local branches.** Anything the backend can legitimately answer with a failure — not found, forbidden, conflict, timeout, offline — is part of the surface's design and belongs in a branch you wrote. 2. **Unexpected failures escalate.** A payload that does not match what the component assumed, or an outright defect during render, has no meaningful local message and should not be papered over with "please try again". 3. **Escalate for region-invalidating failures even when they are expected.** A missing record on a detail surface is expected and still deserves one whole-region treatment rather than five panels each apologising separately. 4. **Never let a failure land in the empty branch.** Catching a rejection and substituting an empty collection removes the failure from the screen and the retry with it. 5. **Never leave a failure silent.** A component that catches, logs and renders nothing produces a blank region nobody can act on, which is worse than either mechanism. ## Distinguish the failures the user can tell apart A single "Something went wrong" for every cause wastes the one useful thing an error branch can do: - **Offline or unreachable** — retrying may genuinely work, and often will as soon as connectivity returns, so a retry is the right control. - **Server refused or errored** — retrying is worth one or two attempts; beyond that the message should stop promising. - **Not authorised** — retry is useless; the useful control is signing in again or asking for access. - **Missing resource** — usually not an error branch at all in user terms, but a not-found state offering navigation back. - **Malformed payload** — retrying repeats the same failure; this is a defect, and the user's best route is out of the surface. Never render the raw failure text or an internal code as the message. A short, specific sentence plus one control is the whole design. ## Retries that do not make it worse Bounded attempts with increasing gaps, a cap, and a hard stop; a manual retry always available; no automatic retry loop that hammers a struggling backend from thousands of clients at once. Retrying a read is safe to repeat; a failed write is not the same decision and belongs with writes rather than here. ## Not this topic How a thrown failure is captured, where the handler must sit relative to the failing component, what kinds of failure never reach it at all, and how a region is reset afterwards are the error-boundary topic's material. What this decision owns is the choice itself and what the user gets on screen at each end of it. ## How this shows up in review - A whole page replaced because one panel's request timed out. - Five sibling panels each rendering the same failure message for one underlying cause. - "Something went wrong" with no retry, or a retry that reloads the document. - A caught rejection rendered as an empty state. - Raw failure text or a status code shown as the message.
- Why can a local error branch offer a better retry than an ancestor handler?Because it still holds the request it made — the identifier, filters, page and sort — so retrying re-issues exactly that one request and leaves the rest of the screen untouched. An ancestor only knows that something beneath it failed, so its honest option is resetting the region, which discards sibling state and anything the user had typed.
- When is escalating the right call even though the failure was entirely expected?When the failure invalidates the whole region. If the record a detail surface describes cannot be loaded, every panel on it is meaningless, and five separate apologies are worse than one region-level state. Escalate, and make that state say something specific about the missing record rather than a generic message.
- What is wrong with one generic failure message for every cause?It throws away the only useful thing the branch can do: point at the next action. Offline wants a retry, unauthorised wants sign-in, a malformed payload wants a way out because retrying repeats it, and a missing record wants navigation. One message means most users get advice that cannot help them.
- Should the error branch retry automatically?A small bounded number of attempts with growing gaps is reasonable for a read, then stop and hand the user a manual retry. Unbounded automatic retries turn a struggling backend into an overwhelmed one, and they hide the failure from the user long enough that the surface just looks slow.
saying these in an interview costs you the question
- Escalates every failure, so one widget takes down the page
- Handles every failure locally, including defects with no sensible message
- Shows one generic message regardless of cause
- Renders raw failure text or a status code to the user
- Expects an ancestor handler to retry the exact failed request
- Catches the failure and renders an empty state instead