koajs/koa · critical · TypeError

non-error thrown: %j

Error message

non-error thrown: %j

What it means

Thrown by Koa's default application error handler (app.onerror / ctx.onerror) when something was thrown that is not a native Error instance. Koa treats every thrown value as a potential error to surface, but the default handler is only safe to inspect Error objects (reading .status, .expose, .stack); a non-error value (string, number, plain object, or a Promise-rejection with a non-Error reason) is re-thrown as a TypeError so the bug is not silently swallowed. The cross-global check (Object.prototype.toString.call(err) === '[object Error]') exists because instanceof Error fails across vm/jest globals (issue #1466). Hitting this means somewhere in your middleware chain (or a dependency) code does throw 'foo' or throw {} instead of throw new Error('foo').

Source

Thrown at lib/application.js:245

    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 52d5e8ff5a)

Solutions

  1. Search the codebase and dependencies for non-Error throws: patterns `throw '`, `throw "`, `throw {`, `throw 40`, `throw 50`, and `Promise.reject(` with a non-Error argument; replace each with `throw new Error(...)` (or an http-errors code via ctx.throw).
  2. In Koa code, never throw raw primitives — use ctx.throw(status, msg) or `throw createError(404, 'not found')` so a real HttpError is produced.
  3. If a third-party dependency throws non-Errors, wrap its calls: try { await lib.x() } catch (e) { throw e instanceof Error ? e : new Error(String(e)) }.
  4. Add an app-level safeguard that normalizes before reaching onerror: process.on('unhandledRejection') and a custom app.on('error') that coerces — but the real fix is at the throw site.
  5. Run with --trace-uncaught / attach a debugger and break on the 'non-error thrown' TypeError to capture the stack back to the original throw.

Example fix

// before
async function handler (ctx) {
  if (!ctx.user) throw 'Unauthorized'        // string thrown -> non-error thrown
  if (!ctx.user.admin) throw 403              // number thrown -> non-error thrown
}

// after
const createError = require('http-errors')
async function handler (ctx) {
  if (!ctx.user) ctx.throw(401, 'Unauthorized')
  if (!ctx.user.admin) ctx.throw(403)
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Normalize any thrown value into a real Error before Koa's onerror ever sees it.
// Install as the FIRST middleware so downstream throws pass through it.
app.use(async (ctx, next) => {
  try {
    await next()
  } catch (err) {
    if (err instanceof Error) throw err
    // Coerce primitives/objects into a real Error so app.onerror never hits 'non-error thrown'
    const e = new Error(typeof err === 'string' ? err : JSON.stringify(err))
    e.exposed = err && typeof err === 'object' && 'status' in err ? err.status : 500
    throw e
  }
})

Type guard

// True cross-global Error guard (mirrors Koa's own check at lib/application.js:242-244)
function isNativeError (e) {
  return e instanceof Error || Object.prototype.toString.call(e) === '[object Error]'
}

// Use before re-throwing inside middleware:
// catch (e) { throw isNativeError(e) ? e : new Error(String(e)) }

// TypeScript narrowing:
// const isErr = (x: unknown): x is Error =>
//   x instanceof Error || Object.prototype.toString.call(x) === '[object Error]'

Try / catch

// App-level safety net: wrap the composed chain so non-Error throws never reach app.onerror
const fn = app.compose(app.middleware)
const safe = async (ctx, next) => {
  try { await fn(ctx, next) }
  catch (err) {
    throw err instanceof Error ? err : new Error(util.format('non-error thrown: %j', err))
  }
}

// Custom error listener that filters by status/expose without ever re-throwing:
app.on('error', (err, ctx) => {
  if (!ctx) return
  if (err.status === 404 || err.expose) return
  logger.error({ err, reqId: ctx.state.reqId }, 'request error')
})

Prevention

When it happens

Trigger: A middleware or downstream function executes `throw 'not found'`, `throw 404`, `throw { status: 404, message: 'x' }`, or rejects a Promise with a non-Error reason (Promise.reject('boom')). A third-party dependency (e.g. an older ORM or driver) throws a plain object/string. ctx.throw() / ctx.assert() are NOT causes — those construct HttpError instances. The trigger is raw throw of a primitive/object inside a request, surfaced through fnMiddleware(ctx).then(...).catch(onerror) -> ctx.onerror -> app.onerror.

Common situations: Legacy JS code ported to Koa that throws strings ('throw "Unauthorized"'); numeric HTTP-status throws (`throw 401`) written by devs confusing ctx.throw(401); libraries that reject with strings/objects rather than Error (some redis/db drivers, older validators); code that wraps errors incorrectly (`catch(e){ throw e.message }`); Jest/VM environments where a thrown Error fails the cross-global instanceof check is NOT the cause here because the toString fallback covers it — the cause is genuinely a non-Error thrown value.

Related errors


AI-assisted analysis of koajs/koa@52d5e8ff5a (2026-08-03). Data as JSON: /data/errors/f9148680642c2705.json. Report an issue: GitHub.