remix-run/react-router · error

The "serverBundles" function must return a string

Error message

The "serverBundles" function must return a string

What it means

The `serverBundles` config lets you assign routes to server bundles via a callback that receives each route branch and must return a bundle ID string. This error fires when the callback returns something else (commonly `undefined` when a code path forgets a return). Bundle IDs drive output directories and Vite environment names, so a non-string is fatal during build-manifest generation.

Solutions

  1. Make `serverBundles` return a string on every code path (end with a default `return 'index'` or similar)
  2. Type the callback so TS flags missing returns: `({ branch }: { branch: ServerBundlesFunctionArgs['branch'] }) => string | Promise<string>`
  3. Use the route branch's ids/files to decide, and log the branch when unsure which path returned undefined

Example fix

// before (react-router.config.ts)
serverBundles: ({ branch }) => {
  if (branch.some((r) => r.id === 'routes/admin')) return 'admin';
},

// after
serverBundles: async ({ branch }) => {
  if (branch.some((r) => r.id === 'routes/admin')) return 'admin';
  return 'index';
},
Defensive patterns

Strategy: type-guard

Validate before calling

// Type the callback so TypeScript rejects missing returns
import type { ServerBundlesFunction } from '@react-router/dev/config';
const serverBundles: ServerBundlesFunction = async ({ branch }) => {
  return branch.some((r) => r.id?.startsWith('routes/admin')) ? 'admin' : 'index';
};
// enable tsconfig option "noImplicitReturns": true to catch fall-through paths

Type guard

const isBundleIdString = (v: unknown): v is string => typeof v === 'string' && v.length > 0;

Try / catch

try {
  await build();
} catch (e) {
  if (e instanceof Error && e.message.includes('must return a string')) {
    // add a default return to every branch of the serverBundles function
  }
}

Prevention

When it happens

Trigger: `serverBundles: ({ branch }) => { if (condition) return 'a'; }` where the fall-through returns undefined; returning a promise of non-string; returning null for branches you meant to leave in the default bundle.

Common situations: Writing a branching function that forgets a default return; refactoring bundle assignment and dropping the final `return`; assuming unlisted routes fall back automatically.

Related errors


AI-assisted analysis of remix-run/react-router@6beaca3952 (2026-08-18). Data as JSON: /api/errors/8d7b2a0349fc8827. Report an issue: GitHub.

Appendix: source

Thrown at packages/react-router-dev/vite/plugin.ts:3465

    serverBundles: {},
    routeIdToServerBundleId: {},
    routes: rootRelativeRoutes,
  };

  await Promise.all(
    getAddressableRoutes(routes).map(async (route) => {
      let branch = getRouteBranch(routes, route.id);
      let serverBundleId = await serverBundles({
        branch: branch.map((route) =>
          configRouteToBranchRoute({
            ...route,
            // Ensure absolute paths are passed to the serverBundles function
            file: path.join(resolvedAppDirectory, route.file),
          }),
        ),
      });
      if (typeof serverBundleId !== "string") {
        throw new Error(`The "serverBundles" function must return a string`);
      }
      // Server bundle IDs must be valid Vite environment names, so hyphens are not allowed
      if (!/^[a-zA-Z0-9_]+$/.test(serverBundleId)) {
        throw new Error(
          `The "serverBundles" function must only return strings containing alphanumeric characters and underscores.`,
        );
      }
      buildManifest.routeIdToServerBundleId[route.id] = serverBundleId;

      buildManifest.serverBundles[serverBundleId] ??= {
        id: serverBundleId,
        file: normalizePath(
          path.join(
            path.relative(
              rootDirectory,
              path.join(serverBuildDirectory, serverBundleId),
            ),
            reactRouterConfig.serverBuildFile,

View on GitHub (pinned to 6beaca3952)