One try/catch at the Top Catches Every Unhandled Error in the Stack
Error-Handling Middleware
Koa's error handling is explicit: wrap the entire pipeline in a top-level try/catch middleware. Learn to use ctx.throw, emit errors to the app event, and return structured JSON or HTML depending on the client.
What you'll learn
- Write a top-level error-catching middleware using try/catch around next()
- Use ctx.throw() to create HTTP errors from route handlers
- Emit errors to the Koa app event emitter for centralized logging
Koa deliberately omits a built-in error handler so that you control the
format of every error response. The pattern is simple: register one
middleware at the very top of the stack with a try/catch that wraps
await next().
The Standard Pattern
import Koa from 'koa';
const app = new Koa();
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || err.statusCode || 500;
ctx.body = {
error: ctx.status < 500 ? err.message : 'Internal Server Error',
};
// Forward to the app-level error event for logging
ctx.app.emit('error', err, ctx);
}
});
// All other middleware and routes go here
app.use(router.routes());
app.on('error', (err, ctx) => {
console.error('server error', err.message, ctx.url);
});
app.listen(3000); Because this middleware is registered first, it is the outermost onion
layer — every throw from any inner layer bubbles up to this catch.
ctx.throw()
Instead of manually constructing Error objects, use ctx.throw() to
create HTTP errors with the correct status attached:
app.use(async (ctx) => {
const item = await db.items.findById(ctx.params.id);
if (!item) ctx.throw(404, 'Item not found');
ctx.body = item;
}); ctx.throw(status, message) creates an error with a .status property,
which the top-level handler reads to set ctx.status correctly.
Content-Negotiated Error Responses
APIs typically want JSON; browsers want HTML. Check ctx.accepts():
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
if (ctx.accepts('json')) {
ctx.body = { error: err.message, status: ctx.status };
} else {
ctx.type = 'text/html';
ctx.body = `<h1>${ctx.status} ${err.message}</h1>`;
}
ctx.app.emit('error', err, ctx);
}
}); Unhandled Rejections Outside Middleware
Some errors (e.g., from startup code) never pass through middleware. Listen
on the app event and on Node’s unhandledRejection to catch them:
app.on('error', (err) => {
console.error('Koa app error:', err);
});
process.on('unhandledRejection', (reason) => {
console.error('Unhandled rejection:', reason);
}); Exposing vs Hiding Details
For 4xx errors (client’s fault) it is safe to echo err.message.
For 5xx errors (server’s fault) never leak stack traces to clients —
log them server-side and return a generic message.
Up Next
Koa’s own package is minimal by design. The next lesson surveys the third-party middleware you will reach for on almost every project.
Third-Party Middleware →