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
- 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).
- In Koa code, never throw raw primitives — use ctx.throw(status, msg) or `throw createError(404, 'not found')` so a real HttpError is produced.
- 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)) }.
- 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.
- 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
- Never throw primitives or plain objects — adopt a project rule: throws are always `new Error()`/`new HttpError()`/`ctx.throw()`.
- CI grep for anti-patterns: `throw ['\"{0-9]` and `Promise.reject('` to catch non-Error throws in your own code.
- Audit third-party drivers/ORMs in use; wrap their calls in a normalizing try/catch (above) so a rogue `throw 'ECONNRESET'` becomes a real Error.
- Write a test that forces a non-Error throw inside a dummy middleware and asserts your normalizing middleware converts it — regression-guards the contract.
- Attach app.on('error', ...) with a structured logger so when a real Error surfaces you also log ctx.method/path/state for triage; keep the listener non-throwing.
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.