Express Async Errors
When building Express.js applications, handling errors in asynchronous code presents unique challenges. In this guide, we'll explore how to effectively manage errors in asynchronous Express route handlers using modern JavaScript techniques and helpful libraries.
The Problem with Async Errors in Express
Express wasn't originally designed with promises and async/await syntax in mind. When using async functions as route handlers, unhandled promise rejections don't automatically propagate to Express error handlers.
Consider this problematic example:
app.get('/users/:id', async (req, res) => {
// This error won't be caught by Express error handlers!
const user = await database.findUser(req.params.id);
if (!user) {
throw new Error('User not found');
}
res.json(user);
});
If database.findUser()
rejects or the "User not found" error is thrown, Express won't catch it. This leads to:
- A hanging request that never completes
- Potential memory leaks
- No proper error response sent to the client
Solution 1: Manual Try-Catch Blocks
The most straightforward approach is wrapping your async code in try-catch blocks:
app.get('/users/:id', async (req, res, next) => {
try {
const user = await database.findUser(req.params.id);
if (!user) {
throw new Error('User not found');
}
res.json(user);
} catch (error) {
next(error); // Forward error to Express error handler
}
});
This works well but becomes tedious when you need to add try-catch blocks to every async route handler.
Solution 2: Higher-Order Function Wrapper
You can create a utility function that wraps your async handlers:
const asyncHandler = fn => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// Using the wrapper
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await database.findUser(req.params.id);
if (!user) {
throw new Error('User not found');
}
res.json(user);
}));
Now errors will be automatically caught and passed to Express error middleware.
Solution 3: express-async-errors Package
The simplest approach is using the express-async-errors
package, which patches Express to handle async errors automatically.
Installation
npm install express-async-errors
Usage
Import it at the very beginning of your app, before defining any routes:
const express = require('express');
require('express-async-errors'); // This patches Express
const app = express();
// Now you can use async route handlers without worry
app.get('/users/:id', async (req, res) => {
const user = await database.findUser(req.params.id);
if (!user) {
throw new Error('User not found');
}
res.json(user);
});
// Define error handler
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: err.message });
});
With express-async-errors
, you can write clean async route handlers without try-catch blocks, and any errors will be automatically forwarded to your Express error handling middleware.
Real-World Example: Building an API with Proper Error Handling
Let's create a small Express application that demonstrates proper async error handling with a more realistic example:
const express = require('express');
require('express-async-errors');
const mongoose = require('mongoose');
const app = express();
app.use(express.json());
// Connect to MongoDB (example)
mongoose.connect('mongodb://localhost/myapp')
.then(() => console.log('Connected to MongoDB'))
.catch(err => console.error('Failed to connect to MongoDB', err));
// Define User model (example)
const User = mongoose.model('User', new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
age: Number
}));
// API Routes
app.get('/api/users', async (req, res) => {
const users = await User.find();
res.json(users);
});
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
// This error will be caught automatically
const error = new Error('User not found');
error.statusCode = 404;
throw error;
}
res.json(user);
});
app.post('/api/users', async (req, res) => {
// Validation could throw errors
const user = new User(req.body);
await user.save();
res.status(201).json(user);
});
// Custom error handler with proper status codes
app.use((err, req, res, next) => {
console.error(err);
const statusCode = err.statusCode || 500;
const message = statusCode === 500 ? 'Internal Server Error' : err.message;
res.status(statusCode).json({
status: 'error',
statusCode,
message
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
In this example:
- We use
express-async-errors
to catch all async errors - Custom errors can include a
statusCode
property - Our error handler formats errors consistently with appropriate status codes
- The route handlers remain clean and focused on the happy path
When to Use Which Approach
- Try-catch blocks: Best for specific error handling logic in individual routes
- Higher-order wrapper function: Good balance of control and DRY code
- express-async-errors: Ideal for most applications, especially when you want global error handling
Performance Considerations
The express-async-errors
package has minimal overhead as it simply patches Express's routing mechanisms. If you're concerned about performance in a high-traffic application, you might benchmark the difference between manual try-catch blocks and the library, but for most applications, the convenience far outweighs any potential performance impact.
Summary
Handling asynchronous errors in Express applications is crucial for building robust, reliable APIs. We've covered three approaches:
- Using manual try-catch blocks with
next(error)
- Creating a higher-order function to wrap async handlers
- Using the
express-async-errors
package for automatic error handling
For most applications, the express-async-errors
package provides the best balance of simplicity and functionality, allowing you to write clean, expressive route handlers without worrying about error propagation.
Additional Resources
- express-async-errors on npm
- Express.js Error Handling documentation
- JavaScript Error Handling with Promises
Exercises
- Convert an existing Express application to use
express-async-errors
- Create a custom error class with status codes and use it in your Express routes
- Implement a logger middleware that records all errors before they reach the error handler
- Build a small REST API with proper async error handling for CRUD operations
By mastering async error handling in Express, you'll build more robust applications that properly respond to errors rather than leaving connections hanging or crashing your server.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)