koajs/koa · error · TypeError

non-error thrown: %j

Error message

non-error thrown: %j

What it means

Re-thrown by app.onerror() when middleware, a promise rejection, or a stream error delivers a value that is not a native Error. Koa's error contract requires real Error instances; the handler does a cross-global-safe check (Object.prototype.toString === '[object Error]' || instanceof Error) to cover jest/vm realms, and when that fails it formats the offending value via util.format('%j') and throws a TypeError so the bad value is never silently swallowed. It surfaces from handleRequest's .catch(onerror) chain, meaning anything a middleware throws or rejects with is funneled here.

Solutions

  1. Find the throw/reject site: search the codebase for throw ' (throw of string literals) and reject( that is not passed a new Error().
  2. Replace string/number/object throws with Error instances: throw new Error('not found') or better throw Object.assign(new Error('not found'), { status: 404 }).
  3. Use Koa's built-in ctx.throw(status, msg) which constructs a proper HttpError with status and expose flags.
  4. If a dependency rejects with non-Errors, wrap the call: try { await lib(x) } catch (e) { throw e instanceof Error ? e : new Error(String(e)) }.
  5. Add a global normalizing middleware last in the stack that catches ctx errors and re-throws as Error so the app.onerror guard never trips.

Example fix

// before
async function auth(ctx, next) {
  if (!ctx.headers.authorization) throw 'unauthorized'   // string, not an Error
  await next()
}

// after
async function auth(ctx, next) {
  if (!ctx.headers.authorization) ctx.throw(401, 'unauthorized')  // builds a native HttpError
  await next()
}
Defensive patterns

Strategy: try-catch

Validate before calling

// normalize anything you are about to throw/reject so app.onerror never sees a non-Error
function toError(value) {
  if (value instanceof Error) return value
  if (typeof value === 'object' && value !== null && typeof value.message === 'string') {
    return Object.assign(new Error(value.message), value)
  }
  return new Error(String(value))
}

// before rejecting
if (!ok) throw toError(reason)

Type guard

// cross-global-safe native Error check matching Koa's own guard
function isNativeError(err) {
  return (
    Object.prototype.toString.call(err) === '[object Error]' ||
    err instanceof Error
  )
}

if (!isNativeError(thrownValue)) {
  // re-wrap before it reaches app.onerror
  throw Object.assign(new Error(String(thrownValue)), { original: thrownValue })
}

Try / catch

// terminal normalizing middleware: register LAST so app.onerror only ever sees native Errors
app.use(async (ctx, next) => {
  try {
    await next()
  } catch (err) {
    if (Object.prototype.toString.call(err) === '[object Error]' || err instanceof Error) {
      throw err // already compliant
    }
    // wrap strings, numbers, plain objects
    const wrapped = new Error(typeof err === 'string' ? err : JSON.stringify(err))
    wrapped.status = (err && err.status) || 500
    wrapped.expose = false
    throw wrapped
  }
})

Prevention

When it happens

Trigger: Any middleware doing throw 'not found', throw 404, throw { status: 400, message: 'bad' }, or return Promise.reject('nope'). A third-party dependency that rejects with a string or plain object. Code that throws the value of an env var or a parsed JSON number. ctx.throw() itself is safe, but a custom helper that does throw res.statusCode (a number) triggers it. A stream piped to the response emitting a non-Error 'error' event.

Common situations: Porting Express-style throw 'bad request' patterns to Koa. Rejecting promises with status codes (reject(401)) in auth helpers. Older Node patterns of throw 'message'. Libraries upgraded to reject with custom error-like objects that fail the cross-global check under jest/jsdom. Assertion libraries that throw non-Error values in certain modes.

Related errors


AI-assisted analysis of koajs/koa@571938d1b4 (2026-08-04). Data as JSON: /api/errors/f9148680642c2705. Report an issue: GitHub.

Appendix: source

Thrown at lib/application.js:260

    context.state = {}
    return context
  }

  /**
   * Default error handler.
   *
   * @param {Error} err
   * @api private
   */

  onerror (err) {
    // When dealing with cross-globals a normal `instanceof` check doesn't work properly.
    // See https://github.com/koajs/koa/issues/1466
    // We can probably remove it once jest fixes https://github.com/facebook/jest/issues/2549.
    const isNativeError =
      Object.prototype.toString.call(err) === '[object Error]' ||
      err instanceof Error
    if (!isNativeError) { throw new TypeError(util.format('non-error thrown: %j', err)) }

    if (err.status === 404 || err.expose) return
    if (this.silent) return

    const msg = err.stack || err.toString()
    console.error(`\n${msg.replace(/^/gm, '  ')}\n`)
  }

  /**
   * Help TS users comply to CommonJS, ESM, bundler mismatch.
   * @see https://github.com/koajs/koa/issues/1513
   */

  static get default () {
    return Application
  }
}

View on GitHub (pinned to 571938d1b4)