{"record":{"id":"1ac8cb2d959f7987","repo":"remix-run/react-router","slug":"you-tried-to-define-routes-asynchronously-but-star","errorCode":null,"errorMessage":"You tried to define routes asynchronously but started defining routes before the async work was done. Please await all async data before calling `defineRoutes()`","messagePattern":"You tried to define routes asynchronously but started defining routes before the async work was done\\. Please await all async data before calling `defineRoutes\\(\\)`","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/react-router-remix-routes-option-adapter/defineRoutes.ts","lineNumber":72,"sourceCode":"}\n\n/**\n * A function for defining routes programmatically, instead of using the\n * filesystem convention.\n */\nexport const defineRoutes: DefineRoutesFunction = (callback) => {\n  let routes: RouteManifest = Object.create(null);\n  let parentRoutes: RouteManifestEntry[] = [];\n  let alreadyReturned = false;\n\n  let defineRoute: DefineRouteFunction = (\n    path,\n    file,\n    optionsOrChildren,\n    children,\n  ) => {\n    if (alreadyReturned) {\n      throw new Error(\n        \"You tried to define routes asynchronously but started defining \" +\n          \"routes before the async work was done. Please await all async \" +\n          \"data before calling `defineRoutes()`\",\n      );\n    }\n\n    let options: DefineRouteOptions;\n    if (typeof optionsOrChildren === \"function\") {\n      // route(path, file, children)\n      options = {};\n      children = optionsOrChildren;\n    } else {\n      // route(path, file, options, children)\n      // route(path, file, options)\n      options = optionsOrChildren || {};\n    }\n\n    let route: RouteManifestEntry = {","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/remix-run/react-router/blob/6beaca39526d5716c3c112ebb0782765baa5a9ce/packages/react-router-remix-routes-option-adapter/defineRoutes.ts#L54-L90","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Do the async work in the enclosing async routes() function, then call defineRoutes with a fully synchronous callback","Pre-fetch data before calling defineRoutes and close over the resolved values","Audit the callback for any promise-returning calls and hoist them out"],"exampleFix":"// before — routes.ts\nexport default {\n  routes: () =>\n    defineRoutes(async () => {\n      const posts = await getPosts(); // callback returns before this resolves\n      for (const p of posts) route(p.slug, `routes/${p.slug}.tsx`);\n    }),\n};\n\n// after\nexport default {\n  routes: async () => {\n    const posts = await getPosts(); // await OUTSIDE defineRoutes\n    return defineRoutes(() => {\n      for (const p of posts) route(p.slug, `routes/${p.slug}.tsx`);\n    });\n  },\n};","handlingStrategy":"validation","validationCode":"// do async work BEFORE defineRoutes so the callback stays synchronous\nexport default {\n  routes: async () => {\n    const posts = await getPosts(); // await first\n    return defineRoutes(() => {\n      for (const p of posts) route(p.slug, `routes/${p.slug}.tsx`);\n    });\n  },\n};","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Keep the defineRoutes callback fully synchronous — no awaits, no .then, no timers","Type the callback parameter as the sync DefineRoutesFunction so async callbacks fail to typecheck","Fetch route metadata in the enclosing async routes() function"],"tags":["routes-config","async","define-routes","remix-adapter"],"backgroundTag":"async-callback-after-return","analyzedSha":"6beaca39526d5716c3c112ebb0782765baa5a9ce","analyzedAt":"2026-08-18T18:04:14.938Z","contentChangedAt":"2026-08-18T18:04:14.938Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}