facebook/react · error · Error
426
426
Error message
A component suspended while responding to synchronous input. This will cause the UI to be replaced with a loading indicator. To fix, updates that suspend should be wrapped with startTransition.
What it means
renderToStringImpl powers the legacy synchronous renderers (renderToString/renderToStaticMarkup). It starts a Fizz request, aborts still-pending Suspense boundaries, and then checks the onShellReady flag; if the shell never became ready — something suspended and blocked synchronous completion — it throws this message (code 426). The source comments themselves note the client-worded message does not really fit here and the legacy renderer is slated for deletion.
Source
Thrown at packages/react-dom/src/server/ReactDOMLegacyServerImpl.js:94
onShellReady,
undefined,
undefined,
undefined,
);
startWork(request);
// If anything suspended and is still pending, we'll abort it before writing.
// That way we write only client-rendered boundaries from the start.
abort(request, abortReason);
startFlowing(request, destination);
if (didFatal && fatalError !== abortReason) {
throw fatalError;
}
if (!readyToStream) {
// Note: This error message is the one we use on the client. It doesn't
// really make sense here. But this is the legacy server renderer, anyway.
// We're going to delete it soon.
throw new Error(
'A component suspended while responding to synchronous input. This ' +
'will cause the UI to be replaced with a loading indicator. To fix, ' +
'updates that suspend should be wrapped with startTransition.',
);
}
return result;
}
export {renderToStringImpl, ReactVersion as version};
View on GitHub (pinned to eafeac097b)
Solutions
- Move to renderToPipeableStream (Node) or renderToReadableStream (edge/browser), which support Suspense and streaming
- Wrap suspending components in <Suspense fallback={...}> so the shell completes without them
- Preload or await the data before calling renderToString so nothing suspends
- Keep lazy()/async resources out of the shell level of the tree
Example fix
// before
const html = renderToString(<App />); // App suspends in the shell
// after
// 1) add a Suspense boundary around the suspending part
<Suspense fallback={<Loading />}><SlowContent /></Suspense>
// 2) or use the streaming API
const {pipe} = renderToPipeableStream(<App />, {onShellReady() { pipe(res); }}); Defensive patterns
Strategy: try-catch
Try / catch
let html;
try {
html = renderToString(<App />);
} catch (e) {
if (
e instanceof Error &&
e.message.includes('A component suspended while responding to synchronous input')
) {
throw new Error(
'renderToString blocked on Suspense — wrap suspending content in <Suspense> or switch to renderToPipeableStream'
);
}
throw e;
} Prevention
- Keep the top of the tree free of lazy()/use()/awaited data when using renderToString
- Wrap any suspending subtree in a <Suspense> boundary with a fallback
- Prefer streaming renderers (renderToPipeableStream/renderToReadableStream) for new SSR code
- Add SSR smoke tests so new suspenseful data loading cannot silently break string rendering
When it happens
Trigger: Calling renderToString() or renderToStaticMarkup() on a tree whose shell suspends and cannot finish synchronously: a lazy() component, awaited promise (use()), or data fetch above any <Suspense> boundary, so onShellReady never fires before the abort.
Common situations: Legacy SSR pipelines feeding renderToString output into page templates; email/HTML generation with components that newly added suspenseful data loading; upgrading trees whose top level now suspends.
Related errors
AI-assisted analysis of facebook/react@eafeac097b (2026-08-21).
Data as JSON: /api/errors/a84d9f1156431410.
Report an issue: GitHub.