Error-Handling Middleware

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.

4 min read Level 3/5 #koa#middleware#error
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 →