remix-run/react-router · error

You tried to define routes asynchronously but started…

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

In @react-router/remix-routes-option-adapter, defineRoutes(callback) invokes the callback synchronously to build the route manifest; the moment the callback returns, alreadyReturned is flipped. If route() is invoked afterwards — from a pending promise continuation, an await resumption, or a timer — the manifest is already frozen, so the adapter throws instead of silently dropping routes you defined late.

Solutions

  1. Do the async work in the enclosing async routes() function, then call defineRoutes with a fully synchronous callback
  2. Pre-fetch data before calling defineRoutes and close over the resolved values
  3. Audit the callback for any promise-returning calls and hoist them out

Example fix

// before — routes.ts
export default {
  routes: () =>
    defineRoutes(async () => {
      const posts = await getPosts(); // callback returns before this resolves
      for (const p of posts) route(p.slug, `routes/${p.slug}.tsx`);
    }),
};

// after
export default {
  routes: async () => {
    const posts = await getPosts(); // await OUTSIDE defineRoutes
    return defineRoutes(() => {
      for (const p of posts) route(p.slug, `routes/${p.slug}.tsx`);
    });
  },
};
Defensive patterns

Strategy: validation

Validate before calling

// do async work BEFORE defineRoutes so the callback stays synchronous
export default {
  routes: async () => {
    const posts = await getPosts(); // await first
    return defineRoutes(() => {
      for (const p of posts) route(p.slug, `routes/${p.slug}.tsx`);
    });
  },
};

Prevention

When it happens

Trigger: Passing a callback that starts async work and calls route() after it resolves (e.g. fetch(...).then(() => route(...)) or an awaited call inside a non-awaited async callback) — the callback itself returns before the async work finishes.

Common situations: Migrating Remix's routes option where route metadata is fetched from a CMS: doing the fetch inside the defineRoutes callback instead of before it; copy-pasting async setup into the callback.

Related errors


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

Appendix: 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 6beaca3952)