Mean 17 Error Handling Middleware
title: "Error Handling Middleware — Building Robust Express APIs" description: "Implement centralized error handling middleware in Express for the MEAN Stack with custom error classes, consistent responses, and logging." weight: 27 date: 2026-06-28 lastmod: 2026-06-28 tags: [frameworks, mean]
Centralized error handling middleware in Express catches errors from all routes, formats them consistently, and logs them for debugging.
What You'll Learn
You will create a centralized error handler with custom error classes, async error wrapper, consistent error response format, and environment-aware error details.
Why It Matters
Without centralized error handling, each route must handle errors individually, leading to inconsistent responses and repetitive code.
Real-World Use
DodaZIP's API uses a centralized error handler that catches all errors, logs them to a monitoring service, and returns consistent JSON responses to the Angular frontend.
flowchart LR
A[Route Handler] --> B{Throws Error?}
B -->|Yes| C[Error Middleware]
B -->|No| D[JSON Response]
C --> E{Error Type}
E --> F[Validation Error]
E --> G[Not Found Error]
E --> H[Auth Error]
E --> I[Server Error]
F --> J[Consistent JSON Error]
G --> J
H --> J
I --> J
J --> K[Log Error]
style C fill:#4a90d9,color:#fff
Custom Error Classes
Create custom error classes with HTTP status codes.
// backend/utils/AppError.js
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} not found`, 404);
}
}
class ValidationError extends AppError {
constructor(message) {
super(message, 400);
}
}
class UnauthorizedError extends AppError {
constructor(message = 'Authentication required') {
super(message, 401);
}
}
class ForbiddenError extends AppError {
constructor(message = 'Insufficient permissions') {
super(message, 403);
}
}
module.exports = {
AppError,
NotFoundError,
ValidationError,
UnauthorizedError,
ForbiddenError
};
Expected output: Custom error classes extend Error with HTTP status codes. Use these classes to throw consistent errors with appropriate status codes.
Async Error Wrapper
Wrap async route handlers to catch errors automatically.
// backend/utils/asyncHandler.js
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
module.exports = asyncHandler;
// Usage
const asyncHandler = require('../utils/asyncHandler');
router.get('/users', asyncHandler(async (req, res) => {
const users = await User.find();
res.json(users);
}));
// Without wrapper, you need try/catch in every route
router.get('/users', async (req, res, next) => {
try {
const users = await User.find();
res.json(users);
} catch (error) {
next(error);
}
});
Expected output: The asyncHandler wraps async route handlers and catches any errors, passing them to the next middleware (the error handler).
Centralized Error Handler Middleware
Create the main error handling middleware.
// backend/middleware/errorHandler.js
const { AppError } = require('../utils/AppError');
function errorHandler(err, req, res, next) {
// Log error
console.error('Error:', err);
// Default to 500 internal server error
let statusCode = err.statusCode || 500;
let message = err.message || 'Internal Server Error';
// Handle specific error types
if (err.name === 'ValidationError') {
// Mongoose validation error
statusCode = 400;
const messages = Object.values(err.errors).map(e => e.message);
message = messages.join('. ');
}
if (err.name === 'CastError') {
// Mongoose invalid ObjectId
statusCode = 400;
message = 'Invalid ID format';
}
if (err.code === 11000) {
// MongoDB duplicate key error
statusCode = 409;
const field = Object.keys(err.keyValue)[0];
message = `Duplicate value for ${field}. This ${field} is already in use.`;
}
if (err.name === 'JsonWebTokenError') {
statusCode = 401;
message = 'Invalid token. Please log in again.';
}
if (err.name === 'TokenExpiredError') {
statusCode = 401;
message = 'Token expired. Please log in again.';
}
if (err.name === 'MulterError') {
statusCode = 400;
if (err.code === 'LIMIT_FILE_SIZE') {
message = 'File too large';
} else {
message = err.message;
}
}
// Response in development vs production
const response = {
success: false,
error: message
};
if (process.env.NODE_ENV === 'development') {
response.stack = err.stack;
}
res.status(statusCode).json(response);
}
module.exports = errorHandler;
Expected output: The error handler catches all errors, determines the appropriate status code, formats the response consistently, and includes stack traces in development.
Using the Error Handler
Apply the error handler in the server file.
// backend/server.js
const errorHandler = require('./middleware/errorHandler');
// Define routes
app.use('/api/users', userRoutes);
app.use('/api/products', productRoutes);
// Error handler MUST be after routes
app.use(errorHandler);
// Handle unhandled routes
app.all('*', (req, res) => {
res.status(404).json({
success: false,
error: `Route ${req.originalUrl} not found`
});
});
Expected output: The error handler is the last middleware. Any error thrown or passed via next() in routes is caught and formatted consistently.
Using Custom Errors in Routes
Throw custom errors in route handlers for consistent error handling.
const asyncHandler = require('../utils/asyncHandler');
const { NotFoundError, ValidationError, ForbiddenError } = require('../utils/AppError');
// Example route using custom errors
router.get('/:id', asyncHandler(async (req, res) => {
const product = await Product.findById(req.params.id);
if (!product) {
throw new NotFoundError('Product');
}
if (product.status === 'archived' && req.user.role !== 'admin') {
throw new ForbiddenError('Archived products can only be viewed by administrators');
}
res.json({ success: true, data: product });
}));
// Example with validation
router.post('/', asyncHandler(async (req, res) => {
const { name, price } = req.body;
if (!name || name.length < 2) {
throw new ValidationError('Product name must be at least 2 characters');
}
if (!price || price <= 0) {
throw new ValidationError('Product price must be positive');
}
const product = await Product.create(req.body);
res.status(201).json({ success: true, data: product });
}));
Expected output: Routes throw typed errors. The error handler catches them and returns appropriate status codes and messages.
Common Mistakes
Not placing error handler after routes: Express middleware runs in order. Error handler must be the last middleware to catch errors from all routes.
Not wrapping async routes: Express does not catch errors from async functions automatically. Without asyncHandler, async errors are unhandled.
Exposing stack traces in production: Stack traces reveal implementation details. Only include them in development responses.
Catching errors and not passing to next(): If you catch an error in a route, pass it to next(error) or throw it. Otherwise, the error handler never sees it.
Not handling specific error types: Mongoose validation errors, CastError, and duplicate key errors have specific error objects. Handle each appropriately.
Practice Questions
- Why should error handling middleware be placed last?
Express executes middleware in order. The error handler must be defined after all routes to catch errors from any route.
- What is the purpose of the asyncHandler wrapper?
It catches errors from async route handlers and passes them to the error handling middleware via next(error).
- How do you differentiate between operational and programmer errors?
Operational errors (invalid input, not found) are expected. Programmer errors (bugs) should crash the process in development.
- What status code should you return for validation errors?
400 Bad Request. Validation errors mean the client sent invalid data.
- How do you handle MongoDB duplicate key errors?
Check for error.code === 11000. Return 409 Conflict with a message indicating which field is duplicated.
Challenge
Implement a complete error handling system with: custom AppError classes, asyncHandler wrapper, centralized error handler that handles ValidationError, CastError, duplicate key, JWT errors, Multer errors, and a 404 handler.
Frequently Asked Questions
{{< faq "How do I handle 404 for unknown routes?" >> Add an app.all('*') handler after all routes that returns a 404 JSON response. {{< /faq >}}
Mini Project
Build a complete error handling system for a REST API with: asyncHandler for all routes, AppError classes for 400, 401, 403, 404, centralized error handler covering Mongoose, JWT, and Multer errors, and a 404 handler for unknown routes.
What's Next
Learn about Deployment options for the MEAN stack application.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro