diff --git a/.changeset/violet-rules-rest.md b/.changeset/violet-rules-rest.md new file mode 100644 index 0000000000..d83ee36ce6 --- /dev/null +++ b/.changeset/violet-rules-rest.md @@ -0,0 +1,5 @@ +--- +"@remix-run/router": patch +--- + +Fix error boundary tracking for multiple errors bubbling to the same boundary diff --git a/packages/router/__tests__/router-test.ts b/packages/router/__tests__/router-test.ts index f8939b6378..ee605a1f10 100644 --- a/packages/router/__tests__/router-test.ts +++ b/packages/router/__tests__/router-test.ts @@ -10774,6 +10774,61 @@ describe("a router", () => { }); }); + it("should handle multiple errors at separate boundaries", async () => { + let routes = [ + { + id: "root", + path: "/", + loader: () => Promise.reject("ROOT"), + hasErrorBoundary: true, + children: [ + { + id: "child", + path: "child", + loader: () => Promise.reject("CHILD"), + hasErrorBoundary: true, + }, + ], + }, + ]; + + let { query } = createStaticHandler(routes); + let context; + + context = await query(createRequest("/child")); + expect(context.errors).toEqual({ + root: "ROOT", + child: "CHILD", + }); + }); + + it("should handle multiple errors at the same boundary", async () => { + let routes = [ + { + id: "root", + path: "/", + loader: () => Promise.reject("ROOT"), + hasErrorBoundary: true, + children: [ + { + id: "child", + path: "child", + loader: () => Promise.reject("CHILD"), + }, + ], + }, + ]; + + let { query } = createStaticHandler(routes); + let context; + + context = await query(createRequest("/child")); + expect(context.errors).toEqual({ + // higher error value wins + root: "ROOT", + }); + }); + it("should handle aborted load requests", async () => { let dfd = createDeferred(); let controller = new AbortController(); diff --git a/packages/router/router.ts b/packages/router/router.ts index 6fa916608b..f62e67d4cf 100644 --- a/packages/router/router.ts +++ b/packages/router/router.ts @@ -2848,9 +2848,14 @@ function processRouteLoaderData( error = Object.values(pendingError)[0]; pendingError = undefined; } - errors = Object.assign(errors || {}, { - [boundaryMatch.route.id]: error, - }); + + errors = errors || {}; + + // Prefer higher error values if lower errors bubble to the same boundary + if (errors[boundaryMatch.route.id] == null) { + errors[boundaryMatch.route.id] = error; + } + // Once we find our first (highest) error, we set the status code and // prevent deeper status codes from overriding if (!foundError) {