In React 19, what actually happens inside React when a component suspends while rendering, and what does that imply about the work the component had already done in that render?
answer
- render is abandoned, not paused
- React unwinds to the nearest boundary
- internally a thenable is thrown
- retry calls the function from the top
- body must be safely re-runnable
basics
~20 sSuspending unwinds that component's render: React abandons the in-progress work, shows the nearest Suspense fallback, subscribes to the pending promise, and runs the component again from the top when it settles. Nothing it rendered is committed.
solid answer
~50 sWhen a component needs a value that is still a pending promise, it *suspends*: React stops rendering that subtree, throws the in-progress work away, and walks up to the nearest `<Suspense>` boundary to show its fallback. React holds onto the promise, and when it settles React schedules a retry that re-runs the component function **from the top** — it does not resume where it stopped. Mechanically this is an unwind: internally React aborts the render by throwing a thenable, which is the same protocol `React.lazy` has always used. In React 19 the supported way to trigger it from application code is `use(promise)`; throwing promises yourself is an internal detail, not an API. The consequence that matters in interviews is purity: because the body re-runs, anything it did the first time — starting a fetch, mutating a module variable, pushing to an array — happens again, and nothing it produced was committed.
code
javascript · 10 lineslet attempts = 0;
function countAttempt() {
attempts += 1;
return attempts;
}
// Called once per render attempt, not once per component instance.
// Under Suspense a single visible render can call this many times.
console.log(countAttempt(), countAttempt());go deeper
Know that a component reading pending data shows the nearest Suspense fallback and that React renders it again once the data arrives. Say plainly that nothing half-rendered reaches the screen.
Be ready to describe the unwind: React abandons the render, records the promise, and re-runs the component from the top when it settles. Explain why that makes render purity a correctness requirement, not a style rule.
Show that you can debug this: duplicated requests, counters that climb, or caches written from render are all symptoms of a body that is not safely re-runnable. Also separate pending (Suspense) from failure (error boundary) when designing a tree.
Own the rule you set for the codebase: render stays pure and side-effect free, resources are created outside render, and every data region carries both a Suspense and an error boundary. Argue why that discipline is what makes concurrent rendering and future retries safe.
## What "suspending" means A React component is a function that must return a description of UI synchronously. Data, by contrast, arrives asynchronously. Suspense is React's answer to that mismatch: a component that cannot finish because a value it needs is still a pending promise *suspends*, and React deals with the gap on its behalf instead of the component inventing its own loading state. The important mental correction is that suspending is not pausing. A generator or an `await` inside an async function pauses at a point and resumes there with its local variables intact. A suspended React component does neither. React unwinds and discards the partial render, and later runs the whole component body again from the first line. ## The unwind Historically the mechanism was literal and visible: a component `throw`s a promise, React catches it during rendering, recognises that the thrown value is a thenable rather than an error, and treats it as "not ready". `React.lazy` has used exactly this protocol since React 16.6. React 19 keeps that unwind model internally, but it is no longer something application code writes; the public API is the `use` hook. When the unwind happens, three things follow: 1. **No commit for that subtree.** The render phase produced work-in-progress fibers, not DOM. Because the render never completed, nothing from it reaches the DOM and no effects for it run. 2. **The nearest boundary takes over.** React walks up the tree to the closest `<Suspense>` ancestor and renders its `fallback` instead. If there is no such ancestor, the error surfaces as "a component suspended while responding to synchronous input" style failure rather than a silent no-op. 3. **React subscribes to the promise.** It attaches a continuation so that when the promise settles — fulfilled *or* rejected — it can schedule another attempt at rendering that subtree. ## The retry re-runs everything On retry, React renders the suspended component again from scratch. It calls the function body top to bottom. It re-runs every hook in order. Any `useMemo` computed during the abandoned render is not carried over — a memo cache belonging to a render that never committed is not something you may rely on. This is why the *purity* rule ("a component must be a pure function of its props, state and context") stops being a style preference and becomes a correctness precondition under Suspense. Consider: ```jsx let requests = 0; function Profile({ id }) { requests++; // runs again on every retry const user = use(userResource(id)); return <h1>{user.name}</h1>; } ``` `requests` is not a count of profiles rendered; it is a count of render attempts. The same trap catches any side effect placed in render: opening a request, writing to a cache without a key, appending to a log. Under Suspense, the body is expected to be safely re-runnable, and React does re-run it. It also explains why the value being read must come from something *outside* the render — a promise created earlier and looked up by key — rather than something created during the render. A freshly created promise on every attempt can never resolve into a completed render. ## What settles the retry A settled promise does not immediately mean success. If it fulfils, the retry reads the value and the component finishes. If it rejects, the retry throws the rejection reason as a real error, which propagates to the nearest error boundary — Suspense handles pending, error boundaries handle failure, and the two are separate mechanisms placed independently in the tree. Also note that React batches retries with the rest of its scheduling. A retry is a normal update at a normal priority, not a synchronous re-render, so several boundaries whose promises settle around the same time can be revealed in the same commit. ## What you can rely on - Suspending discards work; it does not preserve it. - The component function is called again in full, so it must be idempotent. - `use(promise)` is the supported trigger in React 19; `throw promise` is an internal protocol you should not write. - Pending is Suspense's concern; rejection is an error boundary's concern. - Because retries re-run the body, the resource must be stable across attempts — which is what a keyed cache provides.
- If the promise a component suspended on rejects instead of resolving, what happens on the retry?React retries the render, and the read of the settled promise throws the rejection reason as an ordinary error. It propagates to the nearest error boundary, not to the Suspense fallback. Suspense handles the pending state only; failure is a separate mechanism, which is why production trees usually pair a boundary for loading with an error boundary for failure.
- Does a value computed with useMemo during a render that suspended survive to the retry?You cannot rely on it. The render never committed, so React is free to discard that work and recompute on the retry. useMemo is a performance hint, not a cache with a guarantee, and it is especially unsafe to lean on across a suspend. Anything that must be stable across attempts belongs outside render, in a keyed cache.
- What happens if a component suspends and there is no <Suspense> boundary anywhere above it?There is nothing to show a fallback, so React cannot resolve the gap. React reports an error saying a component suspended with no boundary above it rather than silently rendering nothing. In practice every route or data-reading region gets a boundary; an unbounded suspend is treated as a bug, not a default.
Suspending is closer to a database transaction being rolled back and replayed than to a paused video: nothing partial is kept, and the whole unit of work runs again from the beginning.
saying these in an interview costs you the question
- Says the component pauses mid-function and resumes where it stopped
- Thinks React keeps the partial render output and patches it later
- Believes the retry skips hooks that already ran
- Says application code should throw promises directly in React 19
- Assumes a rejected promise shows the Suspense fallback forever