Express API Error Handling
When building REST APIs with Express, proper error handling is crucial for creating robust, reliable, and user-friendly applications. In this guide, we'll explore how to implement effective error handling strategies in your Express applications.
Why Error Handling Matters
Without proper error handling:
- Users receive cryptic error messages or no feedback at all
- Debugging becomes difficult
- Security vulnerabilities may arise from exposed stack traces
- The application might crash entirely
Well-implemented error handling provides:
- Clear feedback to API consumers
- Consistent error responses
- Better application stability
- Easier debugging and maintenance
Basic Express Error Handling
Express comes with a built-in error handler that catches synchronous errors. However, it doesn't handle asynchronous errors by default.
Synchronous Error Handling
Express automatically catches errors in synchronous code:
app.get('/example', (req, res) => {
// This error will be caught by Express
throw new Error('Something went wrong!');
});
Asynchronous Error Handling
For asynchronous operations, you need to pass errors to the next()
function:
app.get('/users/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
res.json(user);
} catch (error) {
// Pass error to Express error handler
next(error);
}
});
Creating a Custom Error Handler Middleware
Express allows you to define custom error-handling middleware with four parameters (instead of the usual three):
// Error handling middleware (must have 4 parameters)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
error: {
message: 'Something went wrong on the server',
// In development, you might want to include more details
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
}
});
});
Creating Custom Error Classes
For more structured error handling, you can create custom error classes:
class APIError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
// Usage
app.get('/resource/:id', async (req, res, next) => {
try {
const resource = await Resource.findById(req.params.id);
if (!resource) {
throw new APIError('Resource not found', 404);
}
res.json(resource);
} catch (error) {
next(error);
}
});
// Modified error handler
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
res.status(statusCode).json({
error: {
message: err.message || 'Internal Server Error',
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
}
});
});
Handling Different Types of Errors
A more comprehensive error handler can distinguish between different types of errors:
app.use((err, req, res, next) => {
// Default to 500 server error
let statusCode = 500;
let message = 'Internal Server Error';
// Handle specific error types
if (err.name === 'ValidationError') {
statusCode = 400;
message = err.message;
} else if (err.name === 'UnauthorizedError') {
statusCode = 401;
message = 'Unauthorized access';
} else if (err instanceof APIError) {
statusCode = err.statusCode;
message = err.message;
} else if (err.name === 'CastError' && err.kind === 'ObjectId') {
statusCode = 400;
message = 'Invalid ID format';
}
// Log error details for debugging
console.error(`${statusCode} - ${message} - ${req.originalUrl} - ${req.method} - ${req.ip}`);
// Send error response
res.status(statusCode).json({
status: 'error',
statusCode,
message,
...(process.env.NODE_ENV === 'development' && {
stack: err.stack,
error: err
})
});
});
Handling 404 Not Found
For routes that don't exist, you can add a middleware that runs after all routes are defined:
// Place this after all your routes
app.use((req, res, next) => {
res.status(404).json({
status: 'error',
message: `Cannot ${req.method} ${req.originalUrl}`
});
});
Handling Express-Validator Errors
If you're using express-validator for input validation, you can create a middleware to handle validation errors:
const { validationResult } = require('express-validator');
// Validation middleware
const validate = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
status: 'error',
statusCode: 400,
message: 'Validation error',
errors: errors.array()
});
}
next();
};
// Usage with a route
const { body } = require('express-validator');
app.post('/users',
[
body('email').isEmail().withMessage('Invalid email'),
body('password').isLength({ min: 6 }).withMessage('Password must be at least 6 characters')
],
validate,
(req, res) => {
// Your controller logic
res.status(201).json({ message: 'User created successfully' });
}
);
Catching Uncaught Exceptions and Unhandled Rejections
For bulletproof error handling, it's important to catch uncaught exceptions and unhandled promise rejections:
// Uncaught exceptions
process.on('uncaughtException', (err) => {
console.error('UNCAUGHT EXCEPTION! 💥 Shutting down...');
console.error(err.name, err.message, err.stack);
// It's best practice to exit the process when an uncaught exception occurs
process.exit(1);
});
// Unhandled promise rejections
process.on('unhandledRejection', (err) => {
console.error('UNHANDLED REJECTION! 💥 Shutting down...');
console.error(err.name, err.message, err.stack);
// Gracefully close server before exiting
server.close(() => {
process.exit(1);
});
});
Real-World Example: Complete Error Handling Setup
Here's a comprehensive example showing how all these pieces fit together in a real Express application:
const express = require('express');
const app = express();
// Custom error class
class APIError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
// Error types for consistency
const ErrorTypes = {
BAD_REQUEST: {
message: 'Bad request',
statusCode: 400
},
UNAUTHORIZED: {
message: 'Unauthorized',
statusCode: 401
},
FORBIDDEN: {
message: 'Forbidden',
statusCode: 403
},
NOT_FOUND: {
message: 'Resource not found',
statusCode: 404
},
INTERNAL: {
message: 'Internal server error',
statusCode: 500
}
};
// Middleware to catch async errors
const asyncHandler = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
// Sample routes
app.get('/api/users', asyncHandler(async (req, res) => {
// Simulating a database operation
const users = [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }];
res.json(users);
}));
app.get('/api/users/:id', asyncHandler(async (req, res) => {
// Simulating a user lookup
const userId = parseInt(req.params.id);
if (isNaN(userId)) {
throw new APIError('Invalid user ID', 400);
}
// Find user (simulated)
const user = userId === 1 ? { id: 1, name: 'John' } : null;
if (!user) {
throw new APIError('User not found', 404);
}
res.json(user);
}));
// Protected resource example
app.get('/api/admin', asyncHandler(async (req, res) => {
// Check for auth header (simplified)
const authHeader = req.headers.authorization;
if (!authHeader) {
throw new APIError('No authorization token provided', 401);
}
// Simulate invalid token
if (authHeader !== 'Bearer valid-token') {
throw new APIError('Invalid or expired token', 401);
}
res.json({ message: 'Admin area accessed successfully' });
}));
// 404 handler for undefined routes
app.use((req, res, next) => {
next(new APIError(`Cannot ${req.method} ${req.originalUrl}`, 404));
});
// Central error handler
app.use((err, req, res, next) => {
console.error(`Error: ${err.message}`);
const statusCode = err.statusCode || 500;
const message = err.message || 'Something went wrong';
res.status(statusCode).json({
status: 'error',
statusCode,
message,
...(process.env.NODE_ENV === 'development' && {
stack: err.stack
})
});
});
// Start server
const PORT = process.env.PORT || 3000;
const server = app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
// Handle unhandled promise rejections
process.on('unhandledRejection', (err) => {
console.error('UNHANDLED REJECTION! 💥');
console.error(err);
server.close(() => {
process.exit(1);
});
});
Testing Error Handling
To verify your error handling works correctly, you can use tools like Postman or write automated tests:
const request = require('supertest');
const app = require('../app');
describe('Error Handling', () => {
test('returns 404 for undefined route', async () => {
const response = await request(app).get('/api/nonexistent');
expect(response.status).toBe(404);
expect(response.body).toHaveProperty('message');
expect(response.body).toHaveProperty('status', 'error');
});
test('returns 400 for invalid ID', async () => {
const response = await request(app).get('/api/users/invalid');
expect(response.status).toBe(400);
expect(response.body.message).toBe('Invalid user ID');
});
test('returns 404 for nonexistent user', async () => {
const response = await request(app).get('/api/users/99');
expect(response.status).toBe(404);
expect(response.body.message).toBe('User not found');
});
});
Summary
Proper error handling is essential for building professional, robust Express APIs. Key takeaways include:
- Always catch and handle errors in asynchronous code
- Create custom error classes for different error types
- Implement a central error-handling middleware
- Handle 404 errors for undefined routes
- Catch uncaught exceptions and unhandled rejections
- Provide user-friendly error messages
- Include detailed information in development but not in production
By implementing these practices, you'll create APIs that are more reliable, easier to debug, and provide better experiences for your users.
Additional Resources
- Express.js Error Handling Documentation
- Mozilla Developer Network - HTTP response status codes
- Best Practices for Express Error Handling
Exercises
- Create a custom error handling middleware that logs errors to a file
- Implement different error responses for API errors vs. database errors
- Build a middleware that validates request parameters and throws appropriate errors
- Create a rate limiting middleware that returns a 429 "Too Many Requests" error
- Implement a feature that sends email notifications for critical server errors
Happy coding!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)