You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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>
Copy file name to clipboardExpand all lines: docs/how-to/suspense.md
+40-1Lines changed: 40 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -68,7 +68,7 @@ export default function MyComponent({
68
68
69
69
## With React 19
70
70
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.
72
72
73
73
```tsx
74
74
<React.Suspensefallback={<div>Loading...</div>}>
@@ -91,3 +91,42 @@ By default, loaders and actions reject any outstanding promises after 4950ms. Yo
91
91
// Reject all pending promises from handler functions after 10 seconds
92
92
exportconst streamTimeout =10_000;
93
93
```
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
+
exportasyncfunction loader() {
106
+
awaitnewPromise((r) =>setTimeout(r, 1000));
107
+
return { parent: "data" };
108
+
}
109
+
110
+
// child.tsx — fast-rejecting streamed promise
111
+
exportasyncfunction loader() {
112
+
let lazy =newPromise((_, reject) =>
113
+
setTimeout(() =>reject(newError("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:
0 commit comments