diff --git a/.changeset/route-lazy-race.md b/.changeset/route-lazy-race.md new file mode 100644 index 0000000000..2306be6926 --- /dev/null +++ b/.changeset/route-lazy-race.md @@ -0,0 +1,5 @@ +--- +"@remix-run/router": patch +--- + +Fix a race-condition with loader/action-thrown errors on route.lazy routes diff --git a/packages/router/__tests__/router-test.ts b/packages/router/__tests__/router-test.ts index 64521aada0..9a2eb9335d 100644 --- a/packages/router/__tests__/router-test.ts +++ b/packages/router/__tests__/router-test.ts @@ -13731,6 +13731,45 @@ describe("a router", () => { ); consoleWarn.mockReset(); }); + + it("handles errors thrown from static loaders before lazy has completed", async () => { + let consoleWarn = jest.spyOn(console, "warn"); + let t = setup({ + routes: [ + { + id: "root", + path: "/", + children: [ + { + id: "lazy", + path: "lazy", + loader: true, + lazy: true, + }, + ], + }, + ], + }); + + let A = await t.navigate("/lazy"); + + await A.loaders.lazy.reject("STATIC LOADER ERROR"); + expect(t.router.state.navigation.state).toBe("loading"); + + // We shouldn't bubble the loader error until after this resolves + // so we know if it has a boundary or not + await A.lazy.lazy.resolve({ + hasErrorBoundary: true, + }); + expect(t.router.state.location.pathname).toBe("/lazy"); + expect(t.router.state.navigation.state).toBe("idle"); + expect(t.router.state.loaderData).toEqual({}); + expect(t.router.state.errors).toEqual({ + lazy: "STATIC LOADER ERROR", + }); + + consoleWarn.mockReset(); + }); }); describe("interruptions", () => { diff --git a/packages/router/router.ts b/packages/router/router.ts index 9dd674eb1f..35b628503a 100644 --- a/packages/router/router.ts +++ b/packages/router/router.ts @@ -3643,10 +3643,19 @@ async function callLoaderOrAction( if (match.route.lazy) { if (handler) { // Run statically defined handler in parallel with lazy() + let handlerError; let values = await Promise.all([ - runHandler(handler), + // If the handler throws, don't let it immediately bubble out, + // since we need to let the lazy() execution finish so we know if this + // route has a boundary that can handle the error + runHandler(handler).catch((e) => { + handlerError = e; + }), loadLazyRouteModule(match.route, mapRouteProperties, manifest), ]); + if (handlerError) { + throw handlerError; + } result = values[0]; } else { // Load lazy route module, then run any returned handler