remix-run/react-router · error · Error

You tried to define routes asynchronously but started defini

Error message

You tried to define routes asynchronously but started defining routes before the async work was done. Please await all async data before calling `defineRoutes()`

What it means

Thrown by defineRoutes() (remix-routes-option-adapter) when the route-defining callback calls the `route` helper AFTER defineRoutes has already returned. defineRoutes is synchronous: it invokes the callback, then sets alreadyReturned = true and returns the manifest. Any async work scheduled inside the callback that later calls route() will hit this guard because the manifest has already been handed back. The API requires all route definitions happen synchronously within the callback.

Source

Thrown at packages/react-router-remix-routes-option-adapter/defineRoutes.ts:72

}

/**
 * A function for defining routes programmatically, instead of using the
 * filesystem convention.
 */
export const defineRoutes: DefineRoutesFunction = (callback) => {
  let routes: RouteManifest = Object.create(null);
  let parentRoutes: RouteManifestEntry[] = [];
  let alreadyReturned = false;

  let defineRoute: DefineRouteFunction = (
    path,
    file,
    optionsOrChildren,
    children,
  ) => {
    if (alreadyReturned) {
      throw new Error(
        "You tried to define routes asynchronously but started defining " +
          "routes before the async work was done. Please await all async " +
          "data before calling `defineRoutes()`",
      );
    }

    let options: DefineRouteOptions;
    if (typeof optionsOrChildren === "function") {
      // route(path, file, children)
      options = {};
      children = optionsOrChildren;
    } else {
      // route(path, file, options, children)
      // route(path, file, options)
      options = optionsOrChildren || {};
    }

    let route: RouteManifestEntry = {

View on GitHub (pinned to 1fd704a7da)

Solutions

  1. Do all async data fetching BEFORE calling defineRoutes — resolve the data, then pass it into a synchronous callback that only calls route().
  2. If you need dynamic routes from an async source, fetch them at build time, write to a JSON file, and have the callback read that file synchronously (fs.readFileSync).
  3. Re-check the @react-router/remix-routes-option-adapter docs: the callback must complete synchronously; restructure so all awaits happen upstream.
  4. Switch to flatRoutes() or a static routes.ts if dynamic async routes aren't truly required.

Example fix

// before — async work inside the callback fires after defineRoutes returns
export default (defineRoutes) => defineRoutes((route) => {
  fetch('/api/routes').then((files) => files.forEach((f) => route(f.path, f.file))); // throws: alreadyReturned
});
// after — resolve async data first, then define synchronously
const files = await fetch('/api/routes').then((r) => r.json());
export default (defineRoutes) => defineRoutes((route) => {
  files.forEach((f) => route(f.path, f.file));
});
Defensive patterns

Strategy: validation

Validate before calling

// ensure the callback passed to defineRoutes performs no await
function isSyncCallback(fn: Function): boolean {
  return fn.constructor.name !== 'AsyncFunction';
}
if (!isSyncCallback(routesCallback)) throw new Error('routes() callback must be synchronous');

Type guard

function isSynchronousRoutesCallback(fn: unknown): boolean {
  return typeof fn === 'function' && fn.constructor.name !== 'AsyncFunction';
}

Prevention

When it happens

Trigger: Inside the routes() callback (the legacy remix `routes` option adapter), the developer awaits a fetch/FS read and then calls route() in a .then() / setTimeout / microtask, after defineRoutes returned. The alreadyReturned flag flips to true the moment the synchronous callback completes.

Common situations: Migrating a Remix v2 `routes` function that loaded route metadata asynchronously (e.g. from a CMS/DB) into the adapter. Forgetting that the legacy routes option is synchronous. Doing `await someAsync()` inside the callback and then continuing to call route().

Related errors


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