How to Handle Errors in an Express Server
Error handling is what separates a learning server from a production one. Here is how to do it right.
How to Handle Errors in an Express Server
Error handling is what separates a learning Express server from a production one. Here is the proper way.
1. Use an Error-Handling Middleware
Add this at the end of app.js with four parameters:
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
error: err.message || 'Something went wrong'
});
});
Express detects the four parameters and uses this middleware for errors. Register it after all routes.
2. Throw or Pass Errors With next(err)
In a handler, throw an error or call next(err). Express jumps straight to the error middleware. Example:
if (!user) return next(new Error('User not found'));
3. Use a Custom Error Class
Subclass Error to add a status and a safe message:
class ApiError extends Error {
constructor(status, message) {
super(message);
this.status = status;
}
}
throw new ApiError(404, 'User not found');
This keeps errors consistent and prevents leaking internals.
4. Catch Async Errors
Async handlers throw promises that Express 4 does not catch. Use a wrapper:
const asyncHandler = fn => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
router.get('/users/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) throw new ApiError(404, 'User not found');
res.json(user);
}));
In Express 5, async errors are caught automatically. The wrapper is still useful for older versions.
5. Use Status Codes Correctly
- 400: bad request (validation).
- 401: not authenticated.
- 403: not authorized.
- 404: not found.
- 409: conflict (duplicate).
- 422: unprocessable entity (semantic error).
- 500: server error.
- 503: maintenance or downstream failure.
6. Never Leak Stack Traces in Production
In development, send the full error. In production, send a safe message. Use a NODE_ENV check:
res.status(err.status || 500).json({
error: process.env.NODE_ENV === 'production'
? 'Something went wrong'
: err.message
});
7. Log Errors Externally
console.error is fine in dev. In production, use a logger (winston, pino) and ship logs to a central place. For real apps, integrate Sentry or similar to get error alerts.
The Takeaway
Handle errors in Express with an error-handling middleware, throw or next(err), a custom ApiError class, an async wrapper, correct status codes, no stack trace leaks in production, and external logging. Done right, errors become informative, not crashes.
Add an error-handling middleware (four parameters) at the end of app.js. Throw or call next(err) in handlers. Use a custom ApiError class with status and message. Use an async wrapper to catch promise rejections. Log externally in production.
Express 4 was designed before async/await and does not catch promise rejections from async handlers. Use an asyncHandler wrapper that catches and passes errors to next. Express 5 catches async errors automatically.
400 bad request, 401 not authenticated, 403 not authorized, 404 not found, 409 conflict (duplicate), 422 unprocessable entity, 500 server error, 503 maintenance or downstream failure.
Stack traces expose internal file paths, library versions, and logic. Attackers can use them to find weaknesses. In production, send a safe message; log the full error internally.
Use a logger like winston or pino. Log JSON with level, timestamp, and context. Ship logs to a central place. For real apps, integrate Sentry (or similar) to get error alerts and breadcrumbs.
Ready to master Node.js completely?
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.
Master Node.js
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course.

