evanw/esbuild · error · Error
Expected onStart() callback in plugin
Error message
Expected onStart() callback in plugin ${quote(name)} to return an object What it means
When an `onStart` callback runs (lib/shared/common.ts:1367), its awaited return value must be `null`/`undefined` (meaning no messages) or a plain object optionally containing `errors` and `warnings` arrays. If it returns a truthy non-object (e.g. a string, number, array, boolean), esbuild throws this error. The thrown error is caught and surfaced as a build error attributed to the named plugin.
Solutions
- Return either `null`/`undefined` or an object `{ errors: [...], warnings: [...] }` from the onStart callback.
- If you only do side effects, simply return nothing (undefined).
Example fix
// before
build.onStart(async () => { return 'starting'; });
// after
build.onStart(async () => { /* side effects */ return null; }); Defensive patterns
Strategy: type-guard
Validate before calling
function wrapOnStart(cb) {
return async () => { const r = await cb(); return r == null || (typeof r === 'object' && !Array.isArray(r)) ? r : null; };
} Type guard
function isOnStartResult(v: any): v is { errors?: any[]; warnings?: any[] } | null {
return v == null || (typeof v === 'object' && !Array.isArray(v));
} Prevention
- Return null explicitly from onStart when you have nothing to report.
- Avoid returning strings/numbers from these hooks.
When it happens
Trigger: An `onStart` callback that returns a string, number, boolean, or an array (instead of an object or null).
Common situations: Returning a status message string by mistake; returning a Promise that resolves to a non-object; forgetting to return `null` after doing work that has no messages.
Related errors
- Expected onEnd() callback in plugin
- Expected onLoad() callback in plugin
- Expected onResolve() callback in plugin
- key in object must be a string
- Must specify "kind" when calling "resolve"
AI-assisted analysis of evanw/esbuild@f6058f8364 (2026-08-09).
Data as JSON: /api/errors/d62e2314485a315f.
Report an issue: GitHub.
Appendix: source
Thrown at lib/shared/common.ts:1367
return { ok: false, error: e, pluginName: name }
}
}
requestCallbacks['on-start'] = async (id, request: protocol.OnStartRequest) => {
// Reset the "pluginData" map before each new build to avoid a memory leak.
// This is done before each new build begins instead of after each build ends
// because I believe the current API doesn't restrict when you can call
// "resolve" and there may be some uses of it that call it around when the
// build ends, and we don't want to accidentally break those use cases.
details.clear()
let response: protocol.OnStartResponse = { errors: [], warnings: [] }
await Promise.all(onStartCallbacks.map(async ({ name, callback, note }) => {
try {
let result = await callback()
if (result != null) {
if (typeof result !== 'object') throw new Error(`Expected onStart() callback in plugin ${quote(name)} to return an object`)
let keys: OptionKeys = {}
let errors = getFlag(result, keys, 'errors', mustBeArray)
let warnings = getFlag(result, keys, 'warnings', mustBeArray)
checkForInvalidFlags(result, keys, `from onStart() callback in plugin ${quote(name)}`)
if (errors != null) response.errors!.push(...sanitizeMessages(errors, 'errors', details, name, undefined))
if (warnings != null) response.warnings!.push(...sanitizeMessages(warnings, 'warnings', details, name, undefined))
}
} catch (e) {
response.errors!.push(extractErrorMessageV8(e, streamIn, details, note && note(), name))
}
}))
sendResponse(id, response as any)
}
requestCallbacks['on-resolve'] = async (id, request: protocol.OnResolveRequest) => {
let response: protocol.OnResolveResponse = {}, name = '', callback, note
for (let id of request.ids) {View on GitHub (pinned to f6058f8364)