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

  1. Return void (undefined) when there's nothing to amend.
  2. Return { errors: [...] } or { warnings: [...] } to add messages.
  3. 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

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


AI-assisted analysis of evanw/esbuild@6ff1d8b0d8 (2026-08-03). Data as JSON: /data/errors/f65c5733482581be.json. Report an issue: GitHub.