Skip to content

Commit b138d64

Browse files
brophdawg11claude
andcommitted
docs(suspense): document early streamed-promise rejections in Node
Streamed promises that reject before all loaders settle escape React Router's handler attachment and crash the process; show the process.on("unhandledRejection") mitigation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7cd8cf8 commit b138d64

1 file changed

Lines changed: 40 additions & 1 deletion

File tree

docs/how-to/suspense.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export default function MyComponent({
6868

6969
## With React 19
7070

71-
If you're experimenting with React 19, you can use `React.use` instead of `Await`, but you'll need to create a new component and pass the promise down to trigger the suspense fallback.
71+
If you're using React 19, you can use `React.use` instead of `Await`, but you'll need to create a new component and pass the promise down to trigger the suspense fallback.
7272

7373
```tsx
7474
<React.Suspense fallback={<div>Loading...</div>}>
@@ -91,3 +91,42 @@ By default, loaders and actions reject any outstanding promises after 4950ms. Yo
9191
// Reject all pending promises from handler functions after 10 seconds
9292
export const streamTimeout = 10_000;
9393
```
94+
95+
## Handling early rejections (Node)
96+
97+
React Router waits for all loaders to settle (via `Promise.all`) before it begins streaming the response. Once streaming has started, React Router catches subsequent rejections of your streamed promises and surfaces them to your `<Await>` (or React 19 `React.use`) error UI.
98+
99+
However, if a streamed promise rejects _before_ all of the route's loaders have settled, React Router has not yet been able to attach a handler to it. In Node, an unhandled promise rejection will crash the process unless you have a top-level handler registered.
100+
101+
For example, this can happen if a parent route's loader takes longer to resolve than a child route's streamed promise takes to reject:
102+
103+
```tsx
104+
// parent.tsx — slow loader
105+
export async function loader() {
106+
await new Promise((r) => setTimeout(r, 1000));
107+
return { parent: "data" };
108+
}
109+
110+
// child.tsx — fast-rejecting streamed promise
111+
export async function loader() {
112+
let lazy = new Promise((_, reject) =>
113+
setTimeout(() => reject(new Error("boom")), 100),
114+
);
115+
return { lazy };
116+
}
117+
```
118+
119+
When `lazy` rejects before the parent loader resolves, the rejection bubbles to the node process as an unhandled rejection, which will crash the process without a user-defined handler.
120+
121+
To prevent this, register a process-level `unhandledRejection` handler in your server entry:
122+
123+
```ts filename=entry.server.ts
124+
process.on("unhandledRejection", (reason, promise) => {
125+
console.error(
126+
"Unhandled Rejection at:",
127+
promise,
128+
"reason:",
129+
reason,
130+
);
131+
});
132+
```

0 commit comments

Comments
 (0)