Skip to main content

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:

javascript
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:

javascript
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):

javascript
// 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:

javascript
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:

javascript
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:

javascript
// 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:

javascript
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:

javascript
// 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:

javascript
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:

javascript
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:

  1. Always catch and handle errors in asynchronous code
  2. Create custom error classes for different error types
  3. Implement a central error-handling middleware
  4. Handle 404 errors for undefined routes
  5. Catch uncaught exceptions and unhandled rejections
  6. Provide user-friendly error messages
  7. 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

Exercises

  1. Create a custom error handling middleware that logs errors to a file
  2. Implement different error responses for API errors vs. database errors
  3. Build a middleware that validates request parameters and throws appropriate errors
  4. Create a rate limiting middleware that returns a 429 "Too Many Requests" error
  5. 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! :)