UNPKG

5.98 kB Markdown View Raw
1---
2title: Error Boundaries
3---
4
5# Error Boundaries
6
7[MODES: framework, data]
8
9To avoid rendering an empty page to users, route modules will automatically catch errors in your code and render the closest `ErrorBoundary`.
10
11Error boundaries are not intended for rendering form validation errors or error reporting. Please see [Form Validation](./form-validation) and [Error Reporting](./error-reporting) instead.
12
13## 1. Add a root error boundary
14
15All applications should at a minimum export a root error boundary. This one handles the three main cases:
16
17- Thrown `data` with a status code and text
18- Instances of errors with a stack trace
19- Randomly thrown values
20
21### Framework Mode
22
23[modes: framework]
24
25In [Framework Mode][picking-a-mode], errors are passed to the route-level error boundary as a prop (see [`Route.ErrorBoundaryProps`][type-safety]), so you don't need to use a hook to grab it:
26
27```tsx filename=root.tsx lines=[1,3-5]
28import { Route } from "./+types/root";
29
30export function ErrorBoundary({
31 error,
32}: Route.ErrorBoundaryProps) {
33 if (isRouteErrorResponse(error)) {
34 return (
35 <>
36 <h1>
37 {error.status} {error.statusText}
38 </h1>
39 <p>{error.data}</p>
40 </>
41 );
42 } else if (error instanceof Error) {
43 return (
44 <div>
45 <h1>Error</h1>
46 <p>{error.message}</p>
47 <p>The stack trace is:</p>
48 <pre>{error.stack}</pre>
49 </div>
50 );
51 } else {
52 return <h1>Unknown Error</h1>;
53 }
54}
55```
56
57### Data Mode
58
59[modes: data]
60
61In [Data Mode][picking-a-mode], the `ErrorBoundary` doesn't receive props, so you can access it via `useRouteError`:
62
63```tsx lines=[1,6,16]
64import { useRouteError } from "react-router";
65
66let router = createBrowserRouter([
67 {
68 path: "/",
69 ErrorBoundary: RootErrorBoundary,
70 Component: Root,
71 },
72]);
73
74function Root() {
75 /* ... */
76}
77
78function RootErrorBoundary() {
79 let error = useRouteError();
80 if (isRouteErrorResponse(error)) {
81 return (
82 <>
83 <h1>
84 {error.status} {error.statusText}
85 </h1>
86 <p>{error.data}</p>
87 </>
88 );
89 } else if (error instanceof Error) {
90 return (
91 <div>
92 <h1>Error</h1>
93 <p>{error.message}</p>
94 <p>The stack trace is:</p>
95 <pre>{error.stack}</pre>
96 </div>
97 );
98 } else {
99 return <h1>Unknown Error</h1>;
100 }
101}
102```
103
104## 2. Write a bug
105
106[modes: framework,data]
107
108It's not recommended to intentionally throw errors to force the error boundary to render as a means of control flow. Error Boundaries are primarily for catching unintentional errors in your code.
109
110```tsx
111export async function loader() {
112 return undefined();
113}
114```
115
116This will render the `instanceof Error` branch of the UI from step 1.
117
118This is not just for loaders, but for all route module APIs: loaders, actions, components, headers, links, and meta.
119
120## 3. Throw data in loaders/actions
121
122[modes: framework,data]
123
124There are exceptions to the rule in #2, especially 404s. You can intentionally `throw data()` (with a proper status code) to the closest error boundary when your loader can't find what it needs to render the page. Throw a 404 and move on.
125
126```tsx
127import { data } from "react-router";
128
129export async function loader({ params }) {
130 let record = await fakeDb.getRecord(params.id);
131 if (!record) {
132 throw data("Record Not Found", { status: 404 });
133 }
134 return record;
135}
136```
137
138This will render the `isRouteErrorResponse` branch of the UI from step 1.
139
140## 4. Nested error boundaries
141
142When an error is thrown, the "closest error boundary" will be rendered.
143
144### Framework Mode
145
146[modes: framework]
147
148Consider these nested routes:
149
150```tsx filename="routes.ts"
151// ✅ has error boundary
152route("/app", "app.tsx", [
153 // ❌ no error boundary
154 route("invoices", "invoices.tsx", [
155 // ✅ has error boundary
156 route("invoices/:id", "invoice-page.tsx", [
157 // ❌ no error boundary
158 route("payments", "payments.tsx"),
159 ]),
160 ]),
161]);
162```
163
164The following table shows which error boundary will render given the origin of the error:
165
166| error origin | rendered boundary |
167| ---------------- | ----------------- |
168| app.tsx | app.tsx |
169| invoices.tsx | app.tsx |
170| invoice-page.tsx | invoice-page.tsx |
171| payments.tsx | invoice-page.tsx |
172
173### Data Mode
174
175[modes: data]
176
177In Data Mode, the equivalent route tree might look like:
178
179```tsx
180let router = createBrowserRouter([
181 {
182 path: "/app",
183 Component: App,
184 ErrorBoundary: AppErrorBoundary, // ✅ has error boundary
185 children: [
186 {
187 path: "invoices",
188 Component: Invoices, // ❌ no error boundary
189 children: [
190 {
191 path: ":id",
192 Component: Invoice,
193 ErrorBoundary: InvoiceErrorBoundary, // ✅ has error boundary
194 children: [
195 {
196 path: "payments",
197 Component: Payments, // ❌ no error boundary
198 },
199 ],
200 },
201 ],
202 },
203 ],
204 },
205]);
206```
207
208The following table shows which error boundary will render given the origin of the error:
209
210| error origin | rendered boundary |
211| ------------ | ---------------------- |
212| `App` | `AppErrorBoundary` |
213| `Invoices` | `AppErrorBoundary` |
214| `Invoice` | `InvoiceErrorBoundary` |
215| `Payments` | `InvoiceErrorBoundary` |
216
217## Error Sanitization
218
219[modes: framework]
220
221In Framework Mode when building for production, any errors that happen on the server are automatically sanitized before being sent to the browser to prevent leaking any sensitive server information (like stack traces).
222
223This means that a thrown `Error` will have a generic message and no stack trace in production in the browser. The original error is untouched on the server.
224
225Also note that data sent with `throw data(yourData)` is not sanitized as the data there is intended to be rendered.
226
227[picking-a-mode]: ../start/modes
228[type-safety]: ../explanation/type-safety