evanw/esbuild · error · Error
Cannot call "resolve" before plugin setup has completed
Error message
Cannot call "resolve" before plugin setup has completed
What it means
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.
Source
Thrown at lib/shared/common.ts:1240
if (typeof item !== 'object') throw new Error(`Plugin at index ${i} must be an object`)
const name = getFlag(item, keys, 'name', mustBeString)
if (typeof name !== 'string' || name === '') throw new Error(`Plugin at index ${i} is missing a name`)
try {
let setup = getFlag(item, keys, 'setup', mustBeFunction)
if (typeof setup !== 'function') throw new Error(`Plugin is missing a setup function`)
checkForInvalidFlags(item, keys, `on plugin ${quote(name)}`)
let plugin: protocol.BuildPlugin = {
name,
onStart: false,
onEnd: false,
onResolve: [],
onLoad: [],
}
i++
let resolve = (path: string, options: types.ResolveOptions = {}): Promise<types.ResolveResult> => {
if (!isSetupDone) throw new Error('Cannot call "resolve" before plugin setup has completed')
if (typeof path !== 'string') throw new Error(`The path to resolve must be a string`)
let keys: OptionKeys = Object.create(null)
let pluginName = getFlag(options, keys, 'pluginName', mustBeString)
let importer = getFlag(options, keys, 'importer', mustBeString)
let namespace = getFlag(options, keys, 'namespace', mustBeString)
let resolveDir = getFlag(options, keys, 'resolveDir', mustBeString)
let kind = getFlag(options, keys, 'kind', mustBeString)
let pluginData = getFlag(options, keys, 'pluginData', canBeAnything)
let importAttributes = getFlag(options, keys, 'with', mustBeObject)
checkForInvalidFlags(options, keys, 'in resolve() call')
return new Promise((resolve, reject) => {
const request: protocol.ResolveRequest = {
command: 'resolve',
path,
key: buildKey,
pluginName: name,
}View on GitHub (pinned to 6ff1d8b0d8)
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.
Example fix
// before
let cachedResolve;
export default {
name: 'p',
setup(build) {
cachedResolve = build.resolve;
},
};
// later, outside setup:
cachedResolve('./x', { kind: 'import-statement' }); // throws
// after
export default {
name: 'p',
setup(build) {
build.onStart(async () => {
const r = await build.resolve('./x', { kind: 'import-statement' });
// use r
return {};
});
},
}; Defensive patterns
Strategy: validation
Validate before calling
// Only call build.resolve from inside plugin callbacks.
export default {
name: 'p',
setup(build) {
build.onStart(async () => {
const r = await build.resolve('./x', { kind: 'import-statement' });
// safe here: setup has completed
return {};
});
},
}; Try / catch
try {
await build.resolve(path, { kind });
} catch (e) {
if (/Cannot call "resolve" before plugin setup/.test(e.message)) {
// defer the resolution into onStart/onLoad
} else throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Plugin at index ${i} must be an object
- Plugin at index ${i} is missing a name
- Plugin is missing a setup function
- Must specify "kind" when calling "resolve"
- You need to wait for the promise returned from "initialize"
AI-assisted analysis of evanw/esbuild@6ff1d8b0d8 (2026-08-03).
Data as JSON: /data/errors/0cb4c5e1ef3e7716.json.
Report an issue: GitHub.