Skip to content

Commit 63ba3e1

Browse files
authored
fix: error handling race condition when using ssr rendering (#74)
There was a race condition in the error handling when the SSR renderer in the worker sent start and error messages back to the main thread. With this PR the error is just saved and sent with the "start" message to mitigate race conditions. This PR also adds a documentation page about the framework-level error boundary component and how to use it to handle errors granularly.
1 parent a27f916 commit 63ba3e1

File tree

5 files changed

+119
-17
lines changed

5 files changed

+119
-17
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
---
2+
title: Error handling
3+
category: Framework
4+
order: 2
5+
---
6+
7+
import Link from "../../../../components/Link.jsx";
8+
9+
# Error handling
10+
11+
You can use the `ErrorBoundary` component to catch errors in your application inside a server component. You can define a fallback component that will be rendered while the error is being handled and a client component that will be rendered when the error occurs.
12+
13+
This is useful when you want to fine-tune the error handling for different parts of your app. You can use any number of `ErrorBoundary` components in your app and each `ErrorBoundary` can have its own fallback component.
14+
15+
```jsx filename="App.jsx"
16+
import { ErrorBoundary } from "@lazarv/react-server/error-boundary";
17+
18+
export default function MyComponent() {
19+
return (
20+
<ErrorBoundary fallback={"Loading..."} component={ErrorMessage}>
21+
<MaybeAnError />
22+
</ErrorBoundary>
23+
);
24+
}
25+
```
26+
27+
The `fallback` prop is a React node that will be rendered while the error is being handled. The `component` prop is a React component that will be rendered when the error occurs. The `fallback` prop is actually used on a `Suspense` component internally, so it's a good practice to use a `Suspense` fallback in the `fallback` prop.
28+
29+
```jsx filename="ErrorMessage.jsx"
30+
"use client";
31+
32+
export default function ErrorMessage({ error }) {
33+
return (
34+
<>
35+
<h1>Error</h1>
36+
<p>{error.message}</p>
37+
<pre>{error.stack}</pre>
38+
</>
39+
);
40+
}
41+
```
42+
43+
You error component passed in the `component` prop of the error boundary component will be rendered in place of the children of the error boundary, where the error occurred. You can render detailed information based on the error or whatever component you would like to, like "uh oh!".
44+
45+
<Link name="reset-error">
46+
## Reset error
47+
</Link>
48+
49+
You can reset the error by calling the `resetErrorBoundary()` function from the error client component if the error occurred on the client.
50+
51+
```jsx filename="ErrorMessage.jsx"
52+
"use client";
53+
54+
export default function ErrorMessage({ error, resetErrorBoundary }) {
55+
return (
56+
<>
57+
<h1>Error</h1>
58+
<p>{error.message}</p>
59+
<pre>{error.stack}</pre>
60+
<button onClick={resetErrorBoundary}>Retry</button>
61+
</>
62+
);
63+
}
64+
```
65+
66+
When the error occurs on the server, you can't reset the error because the error was not thrown on the client. But you can use the `Refresh` component to reload the page. Check it out in more details in the [client-side navigation](/router/client-navigation) page of the [router](/router) section.
67+
68+
```jsx filename="ErrorMessage.jsx"
69+
"use client";
70+
71+
import { Refresh } from "@lazarv/react-server/navigation";
72+
73+
export default function ErrorMessage({ error }) {
74+
return (
75+
<>
76+
<h1>Error</h1>
77+
<p>{error.message}</p>
78+
<pre>{error.stack}</pre>
79+
<Refresh>Retry</Refresh>
80+
</>
81+
);
82+
}
83+
```
84+
85+
<Link name="file-system-based-error-handling">
86+
## File-system based error handling
87+
</Link>
88+
89+
You can learn more about how to handle errors when using the file-system based routing in the [error handling](/router/error-handling) page of the [router](/router) section.

docs/src/pages/en/framework.(index).mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ You can also access the full [HTTP context](/framework/http) during server-side
1010

1111
The framework also provides tools for you to [cache](/framework/caching) data on the server. You can cache data in-memory by default, but you can also build your own cache provider.
1212

13+
For error handling, you can learn about how to use the built-in [error boundary](/framework/error-handling) component and how to implement your own error handling strategy.
14+
1315
You can also learn about some small, but useful modes of the framework in this section, like [partial pre-rendering](/framework/ppr), [cluster mode](/framework/cluster) or [middleware mode](/framework/middleware-mode). Partial pre-rendering is useful when you want to pre-render only parts of your app. Cluster mode is useful when you want to run your app in a multi-process environment. While middleware mode is useful when you want to run your app as a middleware in an existing server, like Express or NestJS.
1416

1517
You can learn about how to implement a micro-frontend architecture using the framework in the [micro-frontends](/framework/micro-frontends) section. The framework provides a set of tools to help you implement micro-frontends in your app. You can use the `RemoteComponent` component to load a micro-frontend from a remote URL and render it in your app using server-side rendering. Server-side rendering supported `iframe` fragments for React applications!

packages/react-server/server/create-worker.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ export function createWorker() {
2020
if (error) {
2121
const err = new Error(error);
2222
err.stack = stack;
23+
if (start) {
24+
workerMap.get(id).start({ id });
25+
}
2326
workerMap.get(id)?.onError?.(err, digest);
2427
} else if (stream) {
2528
workerMap.get(id).resolve(stream);

packages/react-server/server/render-dom.mjs

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const createRenderer = ({
3232
throw new Error("No flight stream provided.");
3333
}
3434
let started = false;
35+
let error = null;
3536
moduleCacheStorage.run(new Map(), async () => {
3637
const linkQueue = new Set();
3738
linkQueueStorage.run(linkQueue, async () => {
@@ -83,23 +84,14 @@ export const createRenderer = ({
8384
html = await resume(tree, postponed, {
8485
formState,
8586
onError(e) {
86-
parentPort.postMessage({
87-
id,
88-
error: e.message,
89-
stack: e.stack,
90-
});
87+
error = e;
9188
},
9289
});
9390
} else {
9491
html = await renderToReadableStream(tree, {
9592
formState,
9693
onError(e) {
97-
parentPort.postMessage({
98-
id,
99-
error: e.message,
100-
stack: e.stack,
101-
digest: e.digest,
102-
});
94+
error = e;
10395
},
10496
});
10597
}
@@ -324,7 +316,13 @@ export const createRenderer = ({
324316

325317
if (!started) {
326318
started = true;
327-
parentPort.postMessage({ id, start: true });
319+
parentPort.postMessage({
320+
id,
321+
start: true,
322+
error: error?.message,
323+
stack: error?.stack,
324+
digest: error?.digest,
325+
});
328326
}
329327
}
330328
};
@@ -380,7 +378,13 @@ export const createRenderer = ({
380378

381379
if (!started) {
382380
started = true;
383-
parentPort.postMessage({ id, start: true });
381+
parentPort.postMessage({
382+
id,
383+
start: true,
384+
error: error?.message,
385+
stack: error?.stack,
386+
digest: error?.digest,
387+
});
384388
}
385389
}
386390
};

packages/react-server/server/render-rsc.jsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@ export async function render(Component) {
439439
const prelude = getContext(PRELUDE_HTML);
440440
const postponed = getContext(POSTPONE_STATE);
441441
const importMap = getContext(IMPORT_MAP);
442+
let isStarted = false;
442443
const stream = await renderStream({
443444
stream: flight,
444445
bootstrapModules: standalone ? [] : getContext(MAIN_MODULE),
@@ -470,6 +471,7 @@ export async function render(Component) {
470471
],
471472
outlet,
472473
start: async () => {
474+
isStarted = true;
473475
ContextStorage.run(contextStore, async () => {
474476
const redirect = getContext(REDIRECT_CONTEXT);
475477
if (redirect?.response) {
@@ -516,10 +518,12 @@ export async function render(Component) {
516518
});
517519
},
518520
onError(e, digest) {
519-
ContextStorage.run(contextStore, async () => {
520-
logger.error(e, digest);
521-
getContext(ERROR_CONTEXT)?.(e)?.then(resolve, reject);
522-
});
521+
logger.error(e, digest);
522+
if (!isStarted) {
523+
ContextStorage.run(contextStore, async () => {
524+
getContext(ERROR_CONTEXT)?.(e)?.then(resolve, reject);
525+
});
526+
}
523527
},
524528
formState,
525529
isPrerender: typeof onPostponed === "function",

0 commit comments

Comments
 (0)