Skip to content

Commit

Permalink
fix: error handling race condition when using ssr rendering (#74)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
lazarv authored Nov 8, 2024
1 parent a27f916 commit 63ba3e1
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 17 deletions.
89 changes: 89 additions & 0 deletions docs/src/pages/en/(pages)/framework/error-handling.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
title: Error handling
category: Framework
order: 2
---

import Link from "../../../../components/Link.jsx";

# Error handling

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.

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.

```jsx filename="App.jsx"
import { ErrorBoundary } from "@lazarv/react-server/error-boundary";

export default function MyComponent() {
return (
<ErrorBoundary fallback={"Loading..."} component={ErrorMessage}>
<MaybeAnError />
</ErrorBoundary>
);
}
```

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.

```jsx filename="ErrorMessage.jsx"
"use client";

export default function ErrorMessage({ error }) {
return (
<>
<h1>Error</h1>
<p>{error.message}</p>
<pre>{error.stack}</pre>
</>
);
}
```

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!".

<Link name="reset-error">
## Reset error
</Link>

You can reset the error by calling the `resetErrorBoundary()` function from the error client component if the error occurred on the client.

```jsx filename="ErrorMessage.jsx"
"use client";

export default function ErrorMessage({ error, resetErrorBoundary }) {
return (
<>
<h1>Error</h1>
<p>{error.message}</p>
<pre>{error.stack}</pre>
<button onClick={resetErrorBoundary}>Retry</button>
</>
);
}
```

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.

```jsx filename="ErrorMessage.jsx"
"use client";

import { Refresh } from "@lazarv/react-server/navigation";

export default function ErrorMessage({ error }) {
return (
<>
<h1>Error</h1>
<p>{error.message}</p>
<pre>{error.stack}</pre>
<Refresh>Retry</Refresh>
</>
);
}
```

<Link name="file-system-based-error-handling">
## File-system based error handling
</Link>

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.
2 changes: 2 additions & 0 deletions docs/src/pages/en/framework.(index).mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ You can also access the full [HTTP context](/framework/http) during server-side

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.

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.

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.

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!
3 changes: 3 additions & 0 deletions packages/react-server/server/create-worker.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export function createWorker() {
if (error) {
const err = new Error(error);
err.stack = stack;
if (start) {
workerMap.get(id).start({ id });
}
workerMap.get(id)?.onError?.(err, digest);
} else if (stream) {
workerMap.get(id).resolve(stream);
Expand Down
30 changes: 17 additions & 13 deletions packages/react-server/server/render-dom.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const createRenderer = ({
throw new Error("No flight stream provided.");
}
let started = false;
let error = null;
moduleCacheStorage.run(new Map(), async () => {
const linkQueue = new Set();
linkQueueStorage.run(linkQueue, async () => {
Expand Down Expand Up @@ -83,23 +84,14 @@ export const createRenderer = ({
html = await resume(tree, postponed, {
formState,
onError(e) {
parentPort.postMessage({
id,
error: e.message,
stack: e.stack,
});
error = e;
},
});
} else {
html = await renderToReadableStream(tree, {
formState,
onError(e) {
parentPort.postMessage({
id,
error: e.message,
stack: e.stack,
digest: e.digest,
});
error = e;
},
});
}
Expand Down Expand Up @@ -324,7 +316,13 @@ export const createRenderer = ({

if (!started) {
started = true;
parentPort.postMessage({ id, start: true });
parentPort.postMessage({
id,
start: true,
error: error?.message,
stack: error?.stack,
digest: error?.digest,
});
}
}
};
Expand Down Expand Up @@ -380,7 +378,13 @@ export const createRenderer = ({

if (!started) {
started = true;
parentPort.postMessage({ id, start: true });
parentPort.postMessage({
id,
start: true,
error: error?.message,
stack: error?.stack,
digest: error?.digest,
});
}
}
};
Expand Down
12 changes: 8 additions & 4 deletions packages/react-server/server/render-rsc.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ export async function render(Component) {
const prelude = getContext(PRELUDE_HTML);
const postponed = getContext(POSTPONE_STATE);
const importMap = getContext(IMPORT_MAP);
let isStarted = false;
const stream = await renderStream({
stream: flight,
bootstrapModules: standalone ? [] : getContext(MAIN_MODULE),
Expand Down Expand Up @@ -470,6 +471,7 @@ export async function render(Component) {
],
outlet,
start: async () => {
isStarted = true;
ContextStorage.run(contextStore, async () => {
const redirect = getContext(REDIRECT_CONTEXT);
if (redirect?.response) {
Expand Down Expand Up @@ -516,10 +518,12 @@ export async function render(Component) {
});
},
onError(e, digest) {
ContextStorage.run(contextStore, async () => {
logger.error(e, digest);
getContext(ERROR_CONTEXT)?.(e)?.then(resolve, reject);
});
logger.error(e, digest);
if (!isStarted) {
ContextStorage.run(contextStore, async () => {
getContext(ERROR_CONTEXT)?.(e)?.then(resolve, reject);
});
}
},
formState,
isPrerender: typeof onPostponed === "function",
Expand Down

0 comments on commit 63ba3e1

Please sign in to comment.