Skip to content

Middleware: errors caught at root error boundary, not route error boundary #13529

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
mpqmpqm opened this issue May 2, 2025 · 7 comments
Closed

Comments

@mpqmpqm
Copy link

mpqmpqm commented May 2, 2025

I'm using React Router as a...

framework

Reproduction

Middleware: errors caught at root error boundary, not route error boundary

Minimal repro

Under certain conditions route-level middleware errors are caught at root.tsx rather than at route-defined error boundaries.

Additionally, different errors are reported for document loads vs. client navigation.

Please note: Comment the loader in the auth layout on and off to see the expected behavior when no loader is defined for that layout and the unexpected behavior when a loader is defined.

Issue template

What version of React Router are you using?

7.5.3

Steps to Reproduce

Minimal repro

  1. Starting at index, click Account link. provideAccount middleware throws and is caught by root.tsx error boundary.

    1. Reload the page to see different errors reported between client navigation and document load.

      Client navigation: Cannot use 'in' operator to search for 'error' in undefined.
      vs. Document load: Expect Error Boundary: account.tsx

  2. Comment out the loader in the auth layout and observe that errors are caught at the expected route error boundaries.

System Info

System:
    OS: macOS 15.4.1
    CPU: (8) arm64 Apple M1
    Memory: 204.30 MB / 16.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 22.11.0 - ~/.nvm/versions/node/v22.11.0/bin/node
    npm: 11.1.0 - ~/.nvm/versions/node/v22.11.0/bin/npm
    pnpm: 10.10.0 - ~/Library/pnpm/pnpm
  Browsers:
    Chrome: 135.0.7049.115
    Edge: 135.0.3179.98
    Safari: 18.4
  npmPackages:
    @react-router/dev: ^7.5.3 => 7.5.3 
    @react-router/node: ^7.5.3 => 7.5.3 
    @react-router/serve: ^7.5.3 => 7.5.3 
    react-router: ^7.5.3 => 7.5.3 
    vite: ^6.3.3 => 6.3.4

Used Package Manager

npm

Expected Behavior

Errors thrown in route-specific middleware should be caught by the route's error boundary. Errors should be consistent between document loads and client navigation.

Actual Behavior

Errors thrown in route-specific middleware are caught by the root error boundary. Different errors are reported for document loads vs. client navigation.

@mpqmpqm mpqmpqm added the bug label May 2, 2025
@brophdawg11
Copy link
Contributor

There 2 things going on here - 1 bug and 1 expected (but unobvious) behavior

The Cannot use 'in' operator to search for 'error' in undefined error you get on client-side navigation is a bug that can should be fixed, but it's not the root issue being questioned here

The "why doesn't my error show on the account error boundary?" question is a different question and in this case is behaving correctly when you do a hard reload and it displays the root error boundary.

  • The order of execution with middleware is: (1) middleware runs top-down, (2) then loaders run and we generate an HTML response, (3) then middleware "bubbles" back up and runs the portions after next() calls
  • On a document request, if a middleware errors in (1) before it calls the next() function, then we short circuit immediately and we ever even run the loaders
  • That means we don't have any data to pass to loaderData on route components
  • So we can't even render any route components for routes that have loader functions
  • So we have to choose an ErrorBoundary accordingly and look no deeper than the first route with a loader
  • On your app, the first route with a loader is auth, so we start looking for an ErrorBoundary there and work upwards and since it doesn't have one we end up at the root ErrorBoundary

@mpqmpqm
Copy link
Author

mpqmpqm commented May 5, 2025

Thanks a lot @brophdawg11 for the quick response. I've got the following funny little solution for binding middleware errors to the nearest error boundary. Maybe you can tell me it's very broken or maybe it gives you an idea.

Thanks also for #13538!

/**
 * Ensures that errors thrown during middleware render at the nearest Error Boundary.
 * https://github.com/remix-run/react-router/issues/13529#issuecomment-2848038105
 * @example
 * const provideAccountContext = async (dataArgs, next) => {
 *  try {
 *    const record = await fetchAccount(dataArgs); // 404
 *    context.set(accountContext, record);
 *  } catch (error) {
 *    await bindMiddlewareError(error, context, next);
 *  }
 * }
 * @example
 * const provideAccountContext = async (dataArgs, next) => {
 *  try {
 *    const record = await fetchAccount(dataArgs); // 404
 *    context.set(accountContext, record);
 *  } catch (error) {
 *    if (error instanceof Fault) {
 *      switch (error.status) {
 *        case 401:
 *        case 404:
 *         // like GitHub, avoid revealing information by differentiating between
 *         // 401 and 404
 *         await bindMiddlewareError(new Fault({ status: 404 }), context, next);
 *         break;
 *       default:
 *         await bindMiddlewareError(error, context, next);
 *    }
 *  }
 * }
 */
export const bindMiddlewareError = async (
  error: unknown,
  context: unstable_RouterContextProvider,
  next: unstable_MiddlewareNextFunction
): Promise<never> => {
  context.set(middlewareErrorContext, error); // middlewares later in chain may skip if prior error
  /**
   * Resolve matched loaders. Awaiting `next` brings us to code path that renders
   * error boundaries. Expect `next` to throw from thwarted expectations due to failed
   * middleware.
   */
  try {
    await next();
    throw error; // sometimes `next` doesn't throw
  } catch (expected) {
    throw error; // ignore the expected error from `next`, instead throw the root error
  }
};

@hilja
Copy link

hilja commented May 6, 2025

I've experienced something similar sounding, but I don't use middleware. This happens sometimes after a fetcher has run and a new post has been added to the page, when I click to view the post (from the parent route) it throws this error:

right-hand side of 'in' should be an object, got undefined

unwrapSingleFetchResult@http://localhost:3000/node_modules/-vite/deps/chunk-A42CNC6T.js?v=1ff46d17:7728:18
singleFetchLoaderFetcherStrategy/result</<@http://localhost:3000/node_modules/.vite/deps/chunk-A42CNC6T.js?v=1ff46d17:7577:14

But only on the first click and not always, impossible to reproduce consistently. I have preloading on intent set.

Not sure if relevant but I'm using react-compiler. v7.5.3.

@brophdawg11
Copy link
Contributor

This should be resolved by #13538 and will be available in the next release

@brophdawg11 brophdawg11 removed their assignment May 6, 2025
@brophdawg11 brophdawg11 added the awaiting release This issue has been fixed and will be released soon label May 6, 2025
@brophdawg11
Copy link
Contributor

@hilja I think your issue is resolved by #13442 which will also be in the next release

Copy link
Contributor

github-actions bot commented May 8, 2025

🤖 Hello there,

We just published version 7.6.0 which involves this issue. If you'd like to take it for a test run please try it out and let us know what you think!

Thanks!

@github-actions github-actions bot removed the awaiting release This issue has been fixed and will be released soon label May 8, 2025
Copy link
Contributor

🤖 Hello there,

We just published version 7.6.1 which involves this issue. If you'd like to take it for a test run please try it out and let us know what you think!

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants