remix-run/react-router · error · Error

Cannot call `runClientMiddleware()` from within an `runClien

Error message

Cannot call `runClientMiddleware()` from within an `runClientMiddleware` handler

What it means

Inside the non-static `runClientMiddleware(cb)` implementation, the `cb` is invoked with a `runClientMiddleware` field that always throws. This forbids recursive nesting: a middleware handler cannot itself call `runClientMiddleware` again, which would create an unbounded pipeline.

Source

Thrown at packages/react-router/lib/router/router.ts:6588

  };
  let runClientMiddleware = isStaticHandler
    ? () => {
        throw new Error(
          "You cannot call `runClientMiddleware()` from a static handler " +
            "`dataStrategy`. Middleware is run outside of `dataStrategy` during " +
            "SSR in order to bubble up the Response.  You can enable middleware " +
            "via the `respond` API in `query`/`queryRoute`",
        );
      }
    : (cb: DataStrategyFunction<RouterContextProvider>) => {
        let typedDataStrategyArgs =
          dataStrategyArgs as DataStrategyFunctionArgs<RouterContextProvider>;
        return runClientMiddlewarePipeline(typedDataStrategyArgs, () => {
          return cb({
            ...typedDataStrategyArgs,
            fetcherKey,
            runClientMiddleware: () => {
              throw new Error(
                "Cannot call `runClientMiddleware()` from within an " +
                  "`runClientMiddleware` handler",
              );
            },
          });
        });
      };

  let results = await dataStrategyImpl({
    ...dataStrategyArgs,
    fetcherKey,
    runClientMiddleware,
  });

  // Wait for all routes to load here but swallow the error since we want
  // it to bubble up from the `await loadRoutePromise` in `callLoaderOrAction` -
  // called from `match.resolve()`. We also ensure that all promises are
  // awaited so that we don't inadvertently leave any hanging promises.

View on GitHub (pinned to 1fd704a7da)

Solutions

  1. Flatten your middleware pipeline into a single `runClientMiddleware(cb)` call per match.
  2. If you need ordered wrappers, compose them inside `cb` directly rather than nesting `runClientMiddleware`.
  3. Move shared logic into a plain function called from `cb`.

Example fix

// before
dataStrategy: (args) =>
  args.runClientMiddleware((a2) =>
    a2.runClientMiddleware(() => defaultStrategy(args)), // throws
  ),

// after
dataStrategy: (args) =>
  args.runClientMiddleware(() => defaultStrategy(args)),
Defensive patterns

Strategy: validation

Validate before calling

let depth = 0;
const safeRunClientMiddleware = (cb) => {
  if (++depth > 1) throw new Error('nested runClientMiddleware');
  try { return runClientMiddleware(cb); } finally { depth--; }
};

Prevention

When it happens

Trigger: Calling `runClientMiddleware(...)` from within the callback passed to a previous `runClientMiddleware(...)` call inside a custom client `dataStrategy`.

Common situations: A dataStrategy that wraps each match with middleware and then attempts to wrap it again for a sub-pipeline; recursive helpers that 'add one more middleware layer' by calling runClientMiddleware inside the handler.

Related errors


AI-assisted analysis of remix-run/react-router@1fd704a7da (2026-08-12). Data as JSON: /api/errors/9175c9b3c641876d. Report an issue: GitHub.