evanw/esbuild · error · Error
Expected onEnd() callback in plugin ${quote(name)} to return
Error message
Expected onEnd() callback in plugin ${quote(name)} to return an object What it means
An onEnd callback receives the final BuildResult and may return { errors?, warnings? } to amend the build's reported messages. lib/shared/common.ts:1496 throws when the callback returns a non-null non-object — e.g. a boolean indicating success, a status string, or a Promise resolving to a primitive. The result-amendment protocol requires the object shape so messages can be sanitised and merged.
Source
Thrown at lib/shared/common.ts:1496
}
let runOnEndCallbacks: RunOnEndCallbacks = (result, done) => done([], [])
if (onEndCallbacks.length > 0) {
runOnEndCallbacks = (result, done) => {
(async () => {
const onEndErrors: types.Message[] = []
const onEndWarnings: types.Message[] = []
for (const { name, callback, note } of onEndCallbacks) {
let newErrors: types.Message[] | undefined
let newWarnings: types.Message[] | undefined
try {
const value = await callback(result)
if (value != null) {
if (typeof value !== 'object') throw new Error(`Expected onEnd() callback in plugin ${quote(name)} to return an object`)
let keys: OptionKeys = {}
let errors = getFlag(value, keys, 'errors', mustBeArray)
let warnings = getFlag(value, keys, 'warnings', mustBeArray)
checkForInvalidFlags(value, keys, `from onEnd() callback in plugin ${quote(name)}`)
if (errors != null) newErrors = sanitizeMessages(errors, 'errors', details, name, undefined)
if (warnings != null) newWarnings = sanitizeMessages(warnings, 'warnings', details, name, undefined)
}
} catch (e) {
newErrors = [extractErrorMessageV8(e, streamIn, details, note && note(), name)]
}
// Try adding the errors and warnings to the result object, but
// continue if something goes wrong. If error-reporting has errors
// then nothing can help us...
if (newErrors) {
onEndErrors.push(...newErrors)
try {View on GitHub (pinned to 6ff1d8b0d8)
Solutions
- Return void (undefined) when there's nothing to amend.
- Return { errors: [...] } or { warnings: [...] } to add messages.
- If signalling failure, convert booleans to { errors: [{ text: '...' }] }.
Example fix
// before
build.onEnd(result => {
return validate(result.outputFiles);
});
// after
build.onEnd(result => {
const problems = validate(result.outputFiles);
return problems.length ? { errors: problems.map(p => ({ text: p })) } : undefined;
}); Defensive patterns
Strategy: validation
Validate before calling
function wrapOnEnd(build, cb) {
build.onEnd(async (result) => {
const v = await cb(result);
if (v != null && typeof v !== 'object') {
throw new TypeError('onEnd callback must return { errors?, warnings? } or void');
}
return v as any;
});
} Type guard
function isEndResult(v): v is { errors?: any[]; warnings?: any[] } {
return v == null || (typeof v === 'object' && !Array.isArray(v));
} Prevention
- Return undefined or { errors, warnings } from onEnd.
- Convert validation booleans into { errors: [{ text }] }.
- Type the callback as (result) => Promise<void | { errors?; warnings? }>.
When it happens
Trigger: onEnd(() => true), onEnd(() => 'ok'), or async onEnd(async () => 'ok'). Returning the raw result of a validation lib that yields a boolean.
Common situations: Plugin performs post-build validation and returns true/false to signal pass/fail instead of returning { errors }. Refactor that surfaces a status code. Author confuses onEnd with a lifecycle hook that returns void.
Related errors
- Expected onStart() callback in plugin ${quote(name)} to retu
- Expected onResolve() callback in plugin ${quote(name)} to re
- Expected onLoad() callback in plugin ${quote(name)} to retur
- Plugin at index ${i} must be an object
- Plugin at index ${i} is missing a name
AI-assisted analysis of evanw/esbuild@6ff1d8b0d8 (2026-08-03).
Data as JSON: /data/errors/f65c5733482581be.json.
Report an issue: GitHub.