Promise-Based Error Handling in Express

Understanding and mastering error handling with the Promise API

Understanding Promise-Based Error Handling

Imagine you're organizing an event with multiple catering services. With async/await, it's like having a coordinator who personally waits at each service point and immediately reports any problems. With Promises and .then(), it's more like setting up a chain of communication channels where messages about success or failure flow through the system. Understanding this difference is crucial for handling errors effectively.

When we work with Promises in Express, we need to understand that errors flow through our application differently than they do with async/await. Let's explore why this happens and how to handle it properly.

Understanding Promise Flow

Before diving into error handling, let's understand how Promises work in Express. Consider this simple Promise-based operation:


// A Promise-based delay function
const delay = (timeToWait) => new Promise((resolve, reject) => {
    setTimeout(() => {
        // Simulating a condition that might cause an error
        if (timeToWait < 0) {
            reject(new Error('Cannot wait for negative time'));
        } else {
            resolve(`Waited for ${timeToWait}ms`);
        }
    }, Math.abs(timeToWait));
});

// A route using this Promise - WITHOUT proper error handling
app.get('/wait', (req, res) => {
    delay(1000)
        .then(response => {
            res.json({ message: response });
        });
    // Notice: No error handling here - this is problematic!
});
            

In this setup, we're only handling the success case. It's like having a plan for when everything goes right, but no contingency for when things go wrong. Let's see what happens when errors occur.

The Challenge with Promise Errors

When using Promises with .then(), errors behave differently than with async/await. Here's what happens when things go wrong:


// This route will cause problems
app.get('/problematic-wait', (req, res) => {
    delay(-1)  // This will trigger an error
        .then(response => {
            res.json({ message: response });
        });
    // The error will be unhandled, causing:
    // 1. Browser hanging indefinitely
    // 2. UnhandledPromiseRejectionWarning in console
    // 3. No proper error response to client
});
            

When an error occurs in this setup, three main problems arise:

1. The browser keeps waiting for a response that will never come

2. The server logs show an unhandled Promise rejection

3. The error never reaches our error handling middleware

Unlike async/await, where express-async-errors can catch thrown errors, Promise rejections need to be handled explicitly in the Promise chain.

Implementing the Solution

The solution is to properly chain our Promise handlers and connect them to Express's error handling system:


// Correct implementation with error handling
app.get('/wait', (req, res, next) => {
    delay(req.query.time)
        .then(response => {
            res.json({ message: response });
        })
        .catch(next);  // This is the key to proper error handling
});

// Let's break down why this works:
// 1. We include 'next' as a parameter in our route handler
// 2. We chain .catch() after .then()
// 3. We pass 'next' directly to .catch()
            

This pattern ensures that any errors in our Promise chain are properly passed to Express's error handling middleware. It's like setting up a safety net that catches and properly processes any errors that occur.

Advanced Promise Error Handling Patterns

Let's explore more sophisticated ways to handle Promise-based errors:

Custom Error Processing


app.get('/advanced-wait', (req, res, next) => {
    delay(req.query.time)
        .then(response => {
            res.json({ message: response });
        })
        .catch(error => {
            // Add custom context to the error
            error.endpoint = '/advanced-wait';
            error.params = req.query;
            error.timestamp = new Date().toISOString();
            
            // Pass the enhanced error to Express
            next(error);
        });
});
            

Multiple Promise Operations


// Handling multiple sequential Promises
app.get('/multiple-operations', (req, res, next) => {
    delay(1000)
        .then(response => {
            // First operation successful
            return delay(2000);  // Chain another operation
        })
        .then(response => {
            // Second operation successful
            res.json({ message: 'All operations completed' });
        })
        .catch(error => {
            // This catch handles errors from ALL previous operations
            error.operationType = 'multiple-delay';
            next(error);
        });
});
            

Practical Real-World Patterns

Here are some common real-world scenarios and how to handle them:

Database Operations


// Database service using Promises
class DatabaseService {
    findUser(id) {
        return db.query('SELECT * FROM users WHERE id = ?', [id])
            .then(results => {
                if (results.length === 0) {
                    throw new Error('User not found');
                }
                return results[0];
            });
    }
}

// Route implementation
app.get('/users/:id', (req, res, next) => {
    const dbService = new DatabaseService();
    
    dbService.findUser(req.params.id)
        .then(user => {
            res.json(user);
        })
        .catch(error => {
            // Customize error based on type
            if (error.message === 'User not found') {
                error.status = 404;
            }
            next(error);
        });
});
            

External API Calls


// API service using Promises
class APIService {
    fetchExternalData(endpoint) {
        return fetch(endpoint)
            .then(response => {
                if (!response.ok) {
                    throw new Error(`API error: ${response.status}`);
                }
                return response.json();
            });
    }
}

// Route implementation
app.get('/external-data', (req, res, next) => {
    const apiService = new APIService();
    
    apiService.fetchExternalData('https://api.example.com/data')
        .then(data => {
            res.json(data);
        })
        .catch(error => {
            error.source = 'external-api';
            next(error);
        });
});
            

Testing Promise Error Handling

Testing Promise-based error handling requires specific approaches:


// tests/promise-error.test.js
describe('Promise Error Handling', () => {
    it('should handle rejected promises correctly', (done) => {
        request(app)
            .get('/wait?time=-1')
            .expect(500)
            .end((err, res) => {
                if (err) return done(err);
                
                expect(res.body).toHaveProperty('error');
                expect(res.body.error).toBe('Cannot wait for negative time');
                done();
            });
    });

    it('should handle multiple promise chains', (done) => {
        request(app)
            .get('/multiple-operations')
            .expect(200)
            .end((err, res) => {
                if (err) return done(err);
                
                expect(res.body.message).toBe('All operations completed');
                done();
            });
    });
});
            

Best Practices and Common Pitfalls

When working with Promise-based error handling, keep these important principles in mind:


// GOOD: Always chain .catch() at the end of Promise chains
app.get('/good-practice', (req, res, next) => {
    somePromiseOperation()
        .then(result => anotherOperation(result))
        .then(finalResult => res.json(finalResult))
        .catch(next);
});

// BAD: Missing error handling
app.get('/bad-practice', (req, res) => {
    somePromiseOperation()
        .then(result => res.json(result));
    // Missing .catch() - errors will be unhandled!
});

// GOOD: Custom error processing when needed
app.get('/better-practice', (req, res, next) => {
    somePromiseOperation()
        .then(result => res.json(result))
        .catch(error => {
            // Add context or transform error if needed
            error.route = '/better-practice';
            error.timestamp = new Date().toISOString();
            next(error);
        });
});