try/catch Around await next() Replaces Four-Argument Express Middleware
Async Flow and Error Handling
Koa's async middleware stack means errors propagate automatically through the promise chain. A single try/catch around await next() handles every error thrown downstream — no callback hell, no forgotten next(err) calls.
What you'll learn
- Catch errors from any downstream middleware with a single try/catch
- Understand why async errors are safer in Koa than in Express
- Write a top-level error-handling middleware for consistent JSON error responses
Koa’s middleware stack is built on promises. Every middleware function returns
a promise, and await next() chains them together. This has one powerful
consequence: a try/catch block around await next() catches any error
thrown anywhere downstream — synchronous or asynchronous.
The Problem With Express Callbacks
In Express, an async error that is not explicitly caught and passed to next
is either silently swallowed or crashes the process:
// Express — easy to forget next(err)
app.get("/user", async (req, res, next) => {
try {
const user = await db.find(req.params.id); // throws
res.json(user);
} catch (err) {
next(err); // you MUST remember this
}
}); The Koa Way — Try/Catch Around await next()
In Koa you write one error-handling middleware at the top of the stack. It
wraps every request in a single try/catch:
import Koa from "koa";
const app = new Koa();
// Top-level error handler
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status ?? 500;
ctx.body = {
error: err.message || "Internal Server Error",
};
ctx.app.emit("error", err, ctx); // also notify the app error event
}
});
// This middleware throws — the error is caught above automatically
app.use(async (ctx) => {
const data = await fetchDataThatMightFail(); // throws
ctx.body = data;
});
app.listen(3000); No matter how deep in the stack an error occurs — or whether it is thrown from
an awaited promise — it travels up the promise chain and is caught by the
outer try/catch.
ctx.throw vs Thrown Errors
ctx.throw creates an HttpError with a status code attached. The error
handler can read err.status to decide the response code:
app.use(async (ctx) => {
if (!ctx.query.id) {
ctx.throw(400, "id is required");
}
}); Regular throw new Error("...") will result in a 500 unless you set
err.status or err.statusCode before throwing.
No More Callback Hell
Because each middleware is simply an async function, you compose them with
plain await — no pyramid of callbacks and no risk of calling next twice or
forgetting it inside a branching code path.
Up Next
Apply what you have learned by structuring a real Koa application across multiple files.
Application Structure →