Skip to content

Throwing in middleware shouldn’t prevent earlier middleware from unwinding #13766

@richardscarrott

Description

@richardscarrott

I'm using React Router as a...

framework

Reproduction

https://stackblitz.com/edit/github-8jsa5pu8?file=app%2Froot.tsx

System Info

System:
    OS: macOS 15.5
    CPU: (10) arm64 Apple M1 Pro
    Memory: 84.63 MB / 32.00 GB
    Shell: 3.7.1 - /opt/homebrew/bin/fish
  Binaries:
    Node: 20.11.0 - ~/.nvm/versions/node/v20.11.0/bin/node
    npm: 10.2.4 - ~/.nvm/versions/node/v20.11.0/bin/npm
    pnpm: 10.11.0 - /opt/homebrew/bin/pnpm
    Watchman: 2025.05.26.00 - /opt/homebrew/bin/watchman
  Browsers:
    Chrome: 137.0.7151.69
    Safari: 18.5
  npmPackages:
    @react-router/dev: ^7.5.3 => 7.6.2
    @react-router/node: ^7.5.3 => 7.6.2
    @react-router/serve: ^7.5.3 => 7.6.2
    react-router: ^7.5.3 => 7.6.2
    vite: ^6.3.3 => 6.3.5

Used Package Manager

npm

Expected Behavior

When middleware throws, earlier middleware that has already awaited next() should still be able to run its after-logic (e.g. modifying the response). For example, if a header is set after await next(), it should be present in the final response, even if later middleware throws.

Actual Behavior

If a later middleware throws, earlier middleware that already awaited next() doesn’t get a chance to run its after-logic. This makes it impossible to reliably apply global headers, logging or cleanup etc.

export const unstable_middleware: Route.unstable_MiddlewareFunction[] = [
  async (_, next) => {
    const response = await next();
    if (!response.headers.has('Cache-Control')) {
      response.headers.set(
        'Cache-Control',
        'no-store, no-cache, must-revalidate, private'
      );
    }
    return response;
  },
  async () => {
    throw new Error(
      "Throwing here shouldn't prevent earlier middleware from unwinding!"
    );
    // EXPECT: 500 response with Cache-Control header
    // ACTUAL: 500 response without Cache-Control header
  },
];

NOTE: Throwing in the loader or <Component /> works as expected; the Cache-Control header is set.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions