Skip to main content

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:

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

  1. A hanging request that never completes
  2. Potential memory leaks
  3. 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:

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

javascript
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

bash
npm install express-async-errors

Usage

Import it at the very beginning of your app, before defining any routes:

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

javascript
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

  1. Try-catch blocks: Best for specific error handling logic in individual routes
  2. Higher-order wrapper function: Good balance of control and DRY code
  3. 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:

  1. Using manual try-catch blocks with next(error)
  2. Creating a higher-order function to wrap async handlers
  3. 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

Exercises

  1. Convert an existing Express application to use express-async-errors
  2. Create a custom error class with status codes and use it in your Express routes
  3. Implement a logger middleware that records all errors before they reach the error handler
  4. 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! :)