Skip to content

Commit fc01722

Browse files
authored
Add location/params arguments to client-side unstable_onError (#14509)
1 parent 7c115a6 commit fc01722

File tree

4 files changed

+277
-35
lines changed

4 files changed

+277
-35
lines changed

.changeset/big-rings-protect.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
[UNSTABLE] Add `location`/`params` as arguments to client-side `unstable_onError` to permit enhanced error reporting.
6+
7+
⚠️ This is a breaking change if you've already adopted `unstable_onError`. The second `errorInfo` parameter is now an object with `location` and `params`:
8+
9+
```tsx
10+
// Before
11+
function errorHandler(error: unknown, errorInfo?: React.errorInfo) {
12+
/*...*/
13+
}
14+
15+
// After
16+
function errorHandler(
17+
error: unknown,
18+
info: {
19+
location: Location;
20+
params: Params;
21+
errorInfo?: React.ErrorInfo;
22+
},
23+
) {
24+
/*...*/
25+
}
26+
```

packages/react-router/__tests__/dom/client-on-error-test.tsx

Lines changed: 199 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ import {
1313
createMemoryRouter,
1414
useFetcher,
1515
useLoaderData,
16+
useRouteError,
1617
} from "../../index";
1718

18-
import { createFormData } from "../router/utils/utils";
19+
import { createFormData, tick } from "../router/utils/utils";
1920
import getHtml from "../utils/getHtml";
2021

2122
describe(`handleError`, () => {
@@ -29,6 +30,155 @@ describe(`handleError`, () => {
2930
consoleError.mockRestore();
3031
});
3132

33+
it("handles hydration lazy errors", async () => {
34+
let spy = jest.fn();
35+
let router = createMemoryRouter([
36+
{
37+
path: "/",
38+
async lazy() {
39+
await tick();
40+
throw new Error("lazy error!");
41+
},
42+
HydrateFallback: () => <h1>Loading...</h1>,
43+
},
44+
]);
45+
46+
let { container } = render(
47+
<RouterProvider router={router} unstable_onError={spy} />,
48+
);
49+
await waitFor(() => screen.getByText("lazy error!"));
50+
51+
expect(spy).toHaveBeenCalledWith(new Error("lazy error!"), {
52+
location: expect.objectContaining({ pathname: "/" }),
53+
params: {},
54+
});
55+
expect(spy).toHaveBeenCalledTimes(1);
56+
expect(getHtml(container)).toContain("Unexpected Application Error!");
57+
});
58+
59+
it("handles hydration middleware errors", async () => {
60+
let spy = jest.fn();
61+
let router = createMemoryRouter([
62+
{
63+
path: "/",
64+
middleware: [
65+
async () => {
66+
await tick();
67+
throw new Error("middleware error!");
68+
},
69+
],
70+
Component: () => <h1>Home</h1>,
71+
ErrorBoundary: () => (
72+
<h1>Error:{(useRouteError() as Error).message}</h1>
73+
),
74+
},
75+
]);
76+
77+
render(<RouterProvider router={router} unstable_onError={spy} />);
78+
79+
await waitFor(() => screen.getByText("Error:middleware error!"));
80+
81+
expect(spy).toHaveBeenCalledWith(new Error("middleware error!"), {
82+
location: expect.objectContaining({ pathname: "/" }),
83+
params: {},
84+
});
85+
expect(spy).toHaveBeenCalledTimes(1);
86+
});
87+
88+
it("handles hydration loader errors", async () => {
89+
let spy = jest.fn();
90+
let router = createMemoryRouter([
91+
{
92+
path: "/",
93+
async loader() {
94+
await tick;
95+
throw new Error("loader error!");
96+
},
97+
Component: () => <h1>Home</h1>,
98+
ErrorBoundary: () => (
99+
<h1>Error:{(useRouteError() as Error).message}</h1>
100+
),
101+
HydrateFallback: () => <h1>Loading...</h1>,
102+
},
103+
]);
104+
105+
render(<RouterProvider router={router} unstable_onError={spy} />);
106+
107+
await waitFor(() => screen.getByText("Error:loader error!"));
108+
109+
expect(spy).toHaveBeenCalledWith(new Error("loader error!"), {
110+
location: expect.objectContaining({ pathname: "/" }),
111+
params: {},
112+
});
113+
expect(spy).toHaveBeenCalledTimes(1);
114+
});
115+
116+
it("handles navigation lazy errors", async () => {
117+
let spy = jest.fn();
118+
let router = createMemoryRouter([
119+
{
120+
path: "/",
121+
Component: () => <h1>Home</h1>,
122+
},
123+
{
124+
id: "page",
125+
path: "/page",
126+
async lazy() {
127+
throw new Error("lazy error!");
128+
},
129+
HydrateFallback: () => <h1>Loading...</h1>,
130+
},
131+
]);
132+
133+
let { container } = render(
134+
<RouterProvider router={router} unstable_onError={spy} />,
135+
);
136+
137+
await act(() => router.navigate("/page"));
138+
139+
expect(spy).toHaveBeenCalledWith(new Error("lazy error!"), {
140+
location: expect.objectContaining({ pathname: "/page" }),
141+
params: {},
142+
});
143+
expect(spy).toHaveBeenCalledTimes(1);
144+
let html = getHtml(container);
145+
expect(html).toContain("Unexpected Application Error!");
146+
expect(html).toContain("lazy error!");
147+
});
148+
149+
it("handles navigation middleware errors", async () => {
150+
let spy = jest.fn();
151+
let router = createMemoryRouter([
152+
{
153+
path: "/",
154+
Component: () => <h1>Home</h1>,
155+
},
156+
{
157+
path: "/page",
158+
middleware: [
159+
() => {
160+
throw new Error("middleware error!");
161+
},
162+
],
163+
Component: () => <h1>Page</h1>,
164+
ErrorBoundary: () => <h1>Error</h1>,
165+
},
166+
]);
167+
168+
let { container } = render(
169+
<RouterProvider router={router} unstable_onError={spy} />,
170+
);
171+
172+
await act(() => router.navigate("/page"));
173+
174+
expect(spy).toHaveBeenCalledWith(new Error("middleware error!"), {
175+
location: expect.objectContaining({ pathname: "/page" }),
176+
params: {},
177+
});
178+
expect(spy).toHaveBeenCalledTimes(1);
179+
expect(getHtml(container)).toContain("Error");
180+
});
181+
32182
it("handles navigation loader errors", async () => {
33183
let spy = jest.fn();
34184
let router = createMemoryRouter([
@@ -52,7 +202,10 @@ describe(`handleError`, () => {
52202

53203
await act(() => router.navigate("/page"));
54204

55-
expect(spy).toHaveBeenCalledWith(new Error("loader error!"));
205+
expect(spy).toHaveBeenCalledWith(new Error("loader error!"), {
206+
location: expect.objectContaining({ pathname: "/page" }),
207+
params: {},
208+
});
56209
expect(spy).toHaveBeenCalledTimes(1);
57210
expect(getHtml(container)).toContain("Error");
58211
});
@@ -85,7 +238,10 @@ describe(`handleError`, () => {
85238
}),
86239
);
87240

88-
expect(spy).toHaveBeenCalledWith(new Error("action error!"));
241+
expect(spy).toHaveBeenCalledWith(new Error("action error!"), {
242+
location: expect.objectContaining({ pathname: "/page" }),
243+
params: {},
244+
});
89245
expect(spy).toHaveBeenCalledTimes(1);
90246
expect(getHtml(container)).toContain("Error");
91247
});
@@ -111,7 +267,10 @@ describe(`handleError`, () => {
111267

112268
await act(() => router.fetch("key", "0", "/fetch"));
113269

114-
expect(spy).toHaveBeenCalledWith(new Error("loader error!"));
270+
expect(spy).toHaveBeenCalledWith(new Error("loader error!"), {
271+
location: expect.objectContaining({ pathname: "/" }),
272+
params: {},
273+
});
115274
expect(spy).toHaveBeenCalledTimes(1);
116275
expect(getHtml(container)).toContain("Error");
117276
});
@@ -142,7 +301,10 @@ describe(`handleError`, () => {
142301
}),
143302
);
144303

145-
expect(spy).toHaveBeenCalledWith(new Error("action error!"));
304+
expect(spy).toHaveBeenCalledWith(new Error("action error!"), {
305+
location: expect.objectContaining({ pathname: "/" }),
306+
params: {},
307+
});
146308
expect(spy).toHaveBeenCalledTimes(1);
147309
expect(getHtml(container)).toContain("Error");
148310
});
@@ -169,12 +331,13 @@ describe(`handleError`, () => {
169331

170332
await act(() => router.navigate("/page"));
171333

172-
expect(spy).toHaveBeenCalledWith(
173-
new Error("render error!"),
174-
expect.objectContaining({
334+
expect(spy).toHaveBeenCalledWith(new Error("render error!"), {
335+
location: expect.objectContaining({ pathname: "/page" }),
336+
params: {},
337+
errorInfo: expect.objectContaining({
175338
componentStack: expect.any(String),
176339
}),
177-
);
340+
});
178341
expect(spy).toHaveBeenCalledTimes(1);
179342
expect(getHtml(container)).toContain("Error");
180343
});
@@ -213,7 +376,10 @@ describe(`handleError`, () => {
213376
await act(() => router.navigate("/page"));
214377
await waitFor(() => screen.getByText("Await Error"));
215378

216-
expect(spy).toHaveBeenCalledWith(new Error("await error!"));
379+
expect(spy).toHaveBeenCalledWith(new Error("await error!"), {
380+
location: expect.objectContaining({ pathname: "/page" }),
381+
params: {},
382+
});
217383
expect(spy).toHaveBeenCalledTimes(1);
218384
expect(getHtml(container)).toContain("Await Error");
219385
});
@@ -258,12 +424,13 @@ describe(`handleError`, () => {
258424
await act(() => router.navigate("/page"));
259425
await waitFor(() => screen.getByText("Await Error"));
260426

261-
expect(spy).toHaveBeenCalledWith(
262-
new Error("await error!"),
263-
expect.objectContaining({
427+
expect(spy).toHaveBeenCalledWith(new Error("await error!"), {
428+
location: expect.objectContaining({ pathname: "/page" }),
429+
params: {},
430+
errorInfo: expect.objectContaining({
264431
componentStack: expect.any(String),
265432
}),
266-
);
433+
});
267434
expect(spy).toHaveBeenCalledTimes(1);
268435
expect(getHtml(container)).toContain("Await Error");
269436
});
@@ -311,13 +478,17 @@ describe(`handleError`, () => {
311478
await act(() => router.navigate("/page"));
312479
await waitFor(() => screen.getByText("Route Error"));
313480

314-
expect(spy).toHaveBeenCalledWith(new Error("await error!"));
315-
expect(spy).toHaveBeenCalledWith(
316-
new Error("errorElement error!"),
317-
expect.objectContaining({
481+
expect(spy).toHaveBeenCalledWith(new Error("await error!"), {
482+
location: expect.objectContaining({ pathname: "/page" }),
483+
params: {},
484+
});
485+
expect(spy).toHaveBeenCalledWith(new Error("errorElement error!"), {
486+
location: expect.objectContaining({ pathname: "/page" }),
487+
params: {},
488+
errorInfo: expect.objectContaining({
318489
componentStack: expect.any(String),
319490
}),
320-
);
491+
});
321492
expect(spy).toHaveBeenCalledTimes(2);
322493
expect(getHtml(container)).toContain("Route Error");
323494
});
@@ -360,7 +531,10 @@ describe(`handleError`, () => {
360531

361532
await act(() => router.navigate("/page"));
362533

363-
expect(spy).toHaveBeenCalledWith(new Error("loader error!"));
534+
expect(spy).toHaveBeenCalledWith(new Error("loader error!"), {
535+
location: expect.objectContaining({ pathname: "/page" }),
536+
params: {},
537+
});
364538
expect(spy).toHaveBeenCalledTimes(1);
365539
expect(getHtml(container)).toContain("Error");
366540

@@ -407,12 +581,13 @@ describe(`handleError`, () => {
407581

408582
await act(() => router.navigate("/page"));
409583

410-
expect(spy).toHaveBeenCalledWith(
411-
new Error("render error!"),
412-
expect.objectContaining({
584+
expect(spy).toHaveBeenCalledWith(new Error("render error!"), {
585+
location: expect.objectContaining({ pathname: "/page" }),
586+
params: {},
587+
errorInfo: expect.objectContaining({
413588
componentStack: expect.any(String),
414589
}),
415-
);
590+
});
416591
expect(spy).toHaveBeenCalledTimes(1);
417592
expect(getHtml(container)).toContain("Error");
418593

0 commit comments

Comments
 (0)