Error handling in Express, the way you'll actually use it
By now your API can respond to requests, but it falls over the moment something goes wrong — a missing field, a database timeout, a typo in a route param. This part fixes that with a single error-handling middleware you'll reuse in every project after this one.
Why default error handling fails
Express catches synchronous errors automatically, but throws them as raw stack traces straight to the client. That's fine on your machine and a liability in production — it can leak file paths, package versions, and internal logic to anyone who trips an error.
If you skip this step, every unhandled error in your API returns a raw HTML stack trace to the caller — including in production. This is the single most common issue we see in first APIs.
Writing an error middleware
Express treats any middleware with four arguments as an error handler. Add this after all your routes, and it becomes the last stop for anything that goes wrong:
function errorHandler(err, req, res, next) {
console.error(err.stack);
const status = err.status || 500;
res.status(status).json({
error: err.message || 'Something went wrong',
});
}
// register it last, after every route
app.use(errorHandler);
Handling async errors
The middleware above only catches synchronous throws. Since most of your route handlers will be async, wrap them so rejected promises get forwarded to the same handler instead of hanging the request:
const asyncHandler = fn => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
app.get('/api/users/:id', asyncHandler(async (req, res) => {
const user = await db.users.find(req.params.id);
res.json(user);
}));
Try it yourself
Add the error handler to your project, then intentionally throw an error inside a route to confirm you get a clean JSON response instead of a stack trace. In the next part, you'll connect a real database and see these handlers catch actual failures.