UNPKG

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