{"id":"0cb4c5e1ef3e7716","repo":"evanw/esbuild","slug":"cannot-call-resolve-before-plugin-setup-has-comp","errorCode":null,"errorMessage":"Cannot call \"resolve\" before plugin setup has completed","messagePattern":"Cannot call \"resolve\" before plugin setup has completed","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"lib/shared/common.ts","lineNumber":1240,"sourceCode":"    if (typeof item !== 'object') throw new Error(`Plugin at index ${i} must be an object`)\n    const name = getFlag(item, keys, 'name', mustBeString)\n    if (typeof name !== 'string' || name === '') throw new Error(`Plugin at index ${i} is missing a name`)\n    try {\n      let setup = getFlag(item, keys, 'setup', mustBeFunction)\n      if (typeof setup !== 'function') throw new Error(`Plugin is missing a setup function`)\n      checkForInvalidFlags(item, keys, `on plugin ${quote(name)}`)\n\n      let plugin: protocol.BuildPlugin = {\n        name,\n        onStart: false,\n        onEnd: false,\n        onResolve: [],\n        onLoad: [],\n      }\n      i++\n\n      let resolve = (path: string, options: types.ResolveOptions = {}): Promise<types.ResolveResult> => {\n        if (!isSetupDone) throw new Error('Cannot call \"resolve\" before plugin setup has completed')\n        if (typeof path !== 'string') throw new Error(`The path to resolve must be a string`)\n        let keys: OptionKeys = Object.create(null)\n        let pluginName = getFlag(options, keys, 'pluginName', mustBeString)\n        let importer = getFlag(options, keys, 'importer', mustBeString)\n        let namespace = getFlag(options, keys, 'namespace', mustBeString)\n        let resolveDir = getFlag(options, keys, 'resolveDir', mustBeString)\n        let kind = getFlag(options, keys, 'kind', mustBeString)\n        let pluginData = getFlag(options, keys, 'pluginData', canBeAnything)\n        let importAttributes = getFlag(options, keys, 'with', mustBeObject)\n        checkForInvalidFlags(options, keys, 'in resolve() call')\n\n        return new Promise((resolve, reject) => {\n          const request: protocol.ResolveRequest = {\n            command: 'resolve',\n            path,\n            key: buildKey,\n            pluginName: name,\n          }","sourceCodeStart":1222,"sourceCodeEnd":1258,"githubUrl":"https://github.com/evanw/esbuild/blob/6ff1d8b0d8c134e867a397eef39702a223ebef9e/lib/shared/common.ts#L1222-L1258","documentation":"esbuild plugins get a resolve() helper inside setup() that performs resolution against the current build context. The guard at lib/shared/common.ts:1240 checks the module-level isSetupDone flag; calling resolve() before the synchronous portion of all plugin setups has finished (e.g. calling it eagerly at module load or from a captured reference outside setup) throws. esbuild serializes plugin setup and only flips isSetupDone afterward, because the build context isn't fully initialised until then.","triggerScenarios":"Saving build.resolve to a variable and invoking it later from outside setup. Calling resolve() at the top level of the plugin module before build() runs. Calling resolve() inside an async setup before awaiting other setup but the engine hasn't set isSetupDone — actually any call before all setups finish.","commonSituations":"Plugin tries to pre-resolve a list of paths in module scope to cache them. Refactor that lifts resolve out of setup into a helper that's invoked eagerly. Misuse of a shared closure that captures resolve and exposes it as a public API.","solutions":["Only call build.resolve(path, {kind}) from within an onStart/onResolve/onLoad/onEnd callback, not eagerly during setup module load.","If you need precomputed resolutions, defer them into onStart() (which runs after setup completes).","Do not stash build.resolve on a module-level variable."],"exampleFix":"// before\nlet cachedResolve;\nexport default {\n  name: 'p',\n  setup(build) {\n    cachedResolve = build.resolve;\n  },\n};\n// later, outside setup:\ncachedResolve('./x', { kind: 'import-statement' }); // throws\n// after\nexport default {\n  name: 'p',\n  setup(build) {\n    build.onStart(async () => {\n      const r = await build.resolve('./x', { kind: 'import-statement' });\n      // use r\n      return {};\n    });\n  },\n};","handlingStrategy":"validation","validationCode":"// Only call build.resolve from inside plugin callbacks.\nexport default {\n  name: 'p',\n  setup(build) {\n    build.onStart(async () => {\n      const r = await build.resolve('./x', { kind: 'import-statement' });\n      // safe here: setup has completed\n      return {};\n    });\n  },\n};","typeGuard":null,"tryCatchPattern":"try {\n  await build.resolve(path, { kind });\n} catch (e) {\n  if (/Cannot call \"resolve\" before plugin setup/.test(e.message)) {\n    // defer the resolution into onStart/onLoad\n  } else throw e;\n}","preventionTips":["Never store build.resolve on a module-level variable.","Move eager resolutions into onStart() which runs after setup completes.","Treat resolve() as a build-time primitive, not a free function."],"tags":["plugins","lifecycle","resolve","api-misuse"],"analyzedSha":"6ff1d8b0d8c134e867a397eef39702a223ebef9e","analyzedAt":"2026-08-03T19:42:38.433Z","schemaVersion":2}