Stopping the Try-Catch Loop: Centralized Error Handling in Express
Stop cluttering your Express routes with repetitive try-catch blocks. Learn how to implement a centralized error-handling middleware for consistent API responses.
27 May 2026, 17:48 UTC

The Repetitive Try-Catch Problem
In many Express applications, route handlers quickly become cluttered with the same pattern: a try-catch block wrapping every asynchronous database call or API request. This repetition obscures the actual business logic and creates a maintenance burden; if you decide to change your error response format, you have to update every single route in the application.
The solution is to shift from local handling (catching errors where they happen) to centralized handling (passing errors to a dedicated middleware). By utilizing Express's built-in error-handling signature, you can ensure every failure across your app results in a consistent JSON response.
The Error-Handling Middleware Signature
Express distinguishes error-handling middleware from standard middleware by the number of arguments. While standard middleware takes three (req, res, next), an error handler must take four: (err, req, res, next).
When you call next(value) with any argument passed into it, Express skips all remaining non-error-handling middleware and jumps directly to the first function defined with those four arguments. This allows you to decouple the detection of an error from the formatting of the response.
Implementing a Standardized Error Flow
To make this system scalable, it is helpful to define a custom Error class. This allows you to attach metadata, such as an HTTP status code, to the error object before it reaches the centralized handler.
// Custom Error Class for operational errors
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
The Centralized Handler Configuration
The error handler must be registered after all your routes and other app.use() calls. If it is placed above a route, it will never be triggered by that route.
// Run this in your main app entry file (e.g., app.js)
const express = require('express');
const app = express();
// 1. Routes
app.get('/user/:id', async (req, res, next) => {
try {
const user = await database.findUser(req.params.id);
if (!user) {
// Trigger the error handler with a custom status
return next(new AppError('User not found', 404));
}
res.json(user);
} catch (err) {
// Pass unexpected errors (DB connection, etc.) to the handler
next(err);
}
});
// 2. Centralized Error Middleware (Must be last)
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
const message = err.message || 'Internal Server Error';
res.status(statusCode).json({
status: 'error',
statusCode,
message,
// Only include stack trace in development
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
});
});
The Async Pitfall in Express 4
A critical limitation of Express 4.x is that it cannot automatically catch errors thrown inside async functions. If an await call rejects and you do not have a try-catch block calling next(err), the request will hang until the client times out, and your process may crash with an UnhandledPromiseRejectionWarning.
To avoid writing try-catch in every route, you can use a wrapper function to catch async errors automatically:
const catchAsync = (fn) => {
return (req, res, next) => {
fn(req, res, next).catch(next);
};
};
// Usage: No try-catch needed inside the handler
app.get('/profile', catchAsync(async (req, res, next) => {
const profile = await fetchProfile();
res.json(profile);
}));
Verification and Risks
To verify your implementation, test three distinct scenarios:
- Synchronous Error: Throw a
new Error()in a route; the handler should return a 500. - Custom Error: Call
next(new AppError('Msg', 400)); the handler should return a 400. - Async Error: Use the
catchAsyncwrapper with a failing promise; the handler should catch it without a manualtry-catch.
Security Warning: Never send err.stack to the client in a production environment. Stack traces reveal your file structure and internal logic, providing a roadmap for attackers.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.