Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/small-flowers-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-router": minor
---

Stabilize `<HydratedRouter onError>`/`<RouterProvider onError>`
2 changes: 1 addition & 1 deletion docs/how-to/error-boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ title: Error Boundaries

To avoid rendering an empty page to users, route modules will automatically catch errors in your code and render the closest `ErrorBoundary`.

Error boundaries are not intended for error reporting or rendering form validation errors. Please see [Form Validation](./form-validation) and [Error Reporting](./error-reporting) instead.
Error boundaries are not intended for rendering form validation errors or error reporting. Please see [Form Validation](./form-validation) and [Error Reporting](./error-reporting) instead.

## 1. Add a root error boundary

Expand Down
90 changes: 84 additions & 6 deletions docs/how-to/error-reporting.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,28 @@ title: Error Reporting

# Error Reporting

[MODES: framework]
[MODES: framework,data]

<br/>
<br/>

React Router catches errors in your route modules and sends them to [error boundaries](./error-boundary) to prevent blank pages when errors occur. However, ErrorBoundary isn't sufficient for logging and reporting errors. To access these caught errors, use the handleError export of the server entry module.
React Router catches errors in your route modules and sends them to [error boundaries](./error-boundary) to prevent blank pages when errors occur. However, `ErrorBoundary` isn't sufficient for logging and reporting errors.

## 1. Reveal the server entry
## Server Errors

If you don't see `entry.server.tsx` in your app directory, you're using a default entry. Reveal it with this cli command:
[modes: framework]

To access these caught errors on the server, use the `handleError` export of the server entry module.

### 1. Reveal the server entry

If you don't see [`entry.server.tsx`][entryserver] in your app directory, you're using a default entry. Reveal it with this cli command:

```shellscript nonumber
react-router reveal
react-router reveal entry.server
```

## 2. Export your error handler
### 2. Export your error handler

This function is called whenever React Router catches an error in your application on the server.

Expand All @@ -39,3 +45,75 @@ export const handleError: HandleErrorFunction = (
}
};
```

## Client Errors

To access these caught errors on the client, use the `onError` prop on your [`HydratedRouter`][hydratedrouter] or [`RouterProvider`][routerprovider] component.

### Framework Mode

[modes: framework]

#### 1. Reveal the client entry

If you don't see [`entry.client.tsx`][entryclient] in your app directory, you're using a default entry. Reveal it with this cli command:

```shellscript nonumber
react-router reveal entry.client
```

#### 2. Add your error handler

This function is called whenever React Router catches an error in your application on the client.

```tsx filename=entry.client.tsx
import { type ClientOnErrorFunction } from "react-router";

const onError: ClientOnErrorFunction = (
error,
{ location, params, unstable_pattern, errorInfo },
) => {
myReportError(error, location, errorInfo);

// make sure to still log the error so you can see it
console.error(error, errorInfo);
};

startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter onError={onError} />
</StrictMode>,
);
});
```

### Data Mode

[modes: data]

This function is called whenever React Router catches an error in your application on the client.

```tsx
import { type ClientOnErrorFunction } from "react-router";

const onError: ClientOnErrorFunction = (
error,
{ location, params, unstable_pattern, errorInfo },
) => {
myReportError(error, location, errorInfo);

// make sure to still log the error so you can see it
console.error(error, errorInfo);
};

function App() {
return <RouterProvider onError={onError} />;
}
```

[entryserver]: ../api/framework-conventions/entry.server.tsx
[entryclient]: ../api/framework-conventions/entry.client.tsx
[hydratedrouter]: ../api//framework-routers/HydratedRouter
[routerprovider]: ../api/data-routers/RouterProvider
2 changes: 1 addition & 1 deletion integration/browser-entry-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ test("allows users to pass an onError function to HydratedRouter", async ({
document,
<StrictMode>
<HydratedRouter
unstable_onError={(error, errorInfo) => {
onError={(error, errorInfo) => {
console.log(error.message, JSON.stringify(errorInfo))
}}
/>
Expand Down
30 changes: 15 additions & 15 deletions packages/react-router/__tests__/dom/client-on-error-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe(`handleError`, () => {
]);

let { container } = render(
<RouterProvider router={router} unstable_onError={spy} />,
<RouterProvider router={router} onError={spy} />,
);
await waitFor(() => screen.getByText("lazy error!"));

Expand Down Expand Up @@ -75,7 +75,7 @@ describe(`handleError`, () => {
},
]);

render(<RouterProvider router={router} unstable_onError={spy} />);
render(<RouterProvider router={router} onError={spy} />);

await waitFor(() => screen.getByText("Error:middleware error!"));

Expand Down Expand Up @@ -104,7 +104,7 @@ describe(`handleError`, () => {
},
]);

render(<RouterProvider router={router} unstable_onError={spy} />);
render(<RouterProvider router={router} onError={spy} />);

await waitFor(() => screen.getByText("Error:loader error!"));

Expand Down Expand Up @@ -134,7 +134,7 @@ describe(`handleError`, () => {
]);

let { container } = render(
<RouterProvider router={router} unstable_onError={spy} />,
<RouterProvider router={router} onError={spy} />,
);

await act(() => router.navigate("/page"));
Expand Down Expand Up @@ -170,7 +170,7 @@ describe(`handleError`, () => {
]);

let { container } = render(
<RouterProvider router={router} unstable_onError={spy} />,
<RouterProvider router={router} onError={spy} />,
);

await act(() => router.navigate("/page"));
Expand Down Expand Up @@ -202,7 +202,7 @@ describe(`handleError`, () => {
]);

let { container } = render(
<RouterProvider router={router} unstable_onError={spy} />,
<RouterProvider router={router} onError={spy} />,
);

await act(() => router.navigate("/page"));
Expand Down Expand Up @@ -234,7 +234,7 @@ describe(`handleError`, () => {
]);

let { container } = render(
<RouterProvider router={router} unstable_onError={spy} />,
<RouterProvider router={router} onError={spy} />,
);

await act(() =>
Expand Down Expand Up @@ -269,7 +269,7 @@ describe(`handleError`, () => {
]);

let { container } = render(
<RouterProvider router={router} unstable_onError={spy} />,
<RouterProvider router={router} onError={spy} />,
);

await act(() => router.fetch("key", "0", "/fetch"));
Expand Down Expand Up @@ -299,7 +299,7 @@ describe(`handleError`, () => {
]);

let { container } = render(
<RouterProvider router={router} unstable_onError={spy} />,
<RouterProvider router={router} onError={spy} />,
);

await act(() =>
Expand Down Expand Up @@ -335,7 +335,7 @@ describe(`handleError`, () => {
]);

let { container } = render(
<RouterProvider router={router} unstable_onError={spy} />,
<RouterProvider router={router} onError={spy} />,
);

await act(() => router.navigate("/page"));
Expand Down Expand Up @@ -380,7 +380,7 @@ describe(`handleError`, () => {
]);

let { container } = render(
<RouterProvider router={router} unstable_onError={spy} />,
<RouterProvider router={router} onError={spy} />,
);

await act(() => router.navigate("/page"));
Expand Down Expand Up @@ -429,7 +429,7 @@ describe(`handleError`, () => {
}

let { container } = render(
<RouterProvider router={router} unstable_onError={spy} />,
<RouterProvider router={router} onError={spy} />,
);

await act(() => router.navigate("/page"));
Expand Down Expand Up @@ -484,7 +484,7 @@ describe(`handleError`, () => {
}

let { container } = render(
<RouterProvider router={router} unstable_onError={spy} />,
<RouterProvider router={router} onError={spy} />,
);

await act(() => router.navigate("/page"));
Expand Down Expand Up @@ -540,7 +540,7 @@ describe(`handleError`, () => {
]);

let { container } = render(
<RouterProvider router={router} unstable_onError={spy} />,
<RouterProvider router={router} onError={spy} />,
);

await act(() => router.navigate("/page"));
Expand Down Expand Up @@ -591,7 +591,7 @@ describe(`handleError`, () => {
]);

let { container } = render(
<RouterProvider router={router} unstable_onError={spy} />,
<RouterProvider router={router} onError={spy} />,
);

await act(() => router.navigate("/page"));
Expand Down
2 changes: 1 addition & 1 deletion packages/react-router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export { AwaitContextProvider as UNSAFE_AwaitContextProvider } from "./lib/conte
export type {
AwaitProps,
IndexRouteProps,
unstable_ClientOnErrorFunction,
ClientOnErrorFunction,
LayoutRouteProps,
MemoryRouterOpts,
MemoryRouterProps,
Expand Down
Loading