When working with asynchronous operations in Express, errors can become tricky to handle. Think of it like trying to catch water flowing through multiple pipes - if there's a leak anywhere along the way, we need to make sure we catch it properly. Our specific challenges are:
1. Handle errors from async/await operations
2. Handle errors from Promise-based operations
3. Prevent unhandled promise rejections that leave the browser hanging
4. Ensure proper error responses reach the client
Let's first look at our delay function to understand what we're working with:
// This is our potentially problematic async operation
const delay = (timeToWait, message) => new Promise((resolve, reject) => {
setTimeout(() => {
// Randomly fails 50% of the time or if timeToWait is negative
if (timeToWait < 0 || Math.random() < 0.5) {
reject(new Error('A delay error has occurred!'));
} else {
resolve(message || `All done waiting for ${timeToWait}ms!`);
}
}, Math.abs(timeToWait));
});
This is like setting up a timer that might randomly fail - imagine a kitchen timer that sometimes rings and sometimes breaks. We need to handle both scenarios gracefully.
First, let's look at our async/await route and understand why it needs special handling:
// Initial problematic version
app.get('/wait1sec', async (req, res) => {
const response = await delay.wait1();
res.json({
message: response,
});
});
// Solution: Add express-async-errors at the top of your application
const express = require('express');
require('express-async-errors'); // This line is crucial!
const app = express();
The express-async-errors package works like a safety net, catching any errors that might fall through our async operations. It's like adding automatic error detection throughout our plumbing system.
For Promise-based operations, we need a different approach:
// Initial problematic version
app.get('/wait4me', (req, res, next) => {
delay.wait4()
.then(response => {
res.json({
message: response,
});
});
});
// Solution: Add .catch(next)
app.get('/wait4me', (req, res, next) => {
delay.wait4()
.then(response => {
res.json({
message: response,
});
})
.catch(next); // This line is crucial!
});
Adding .catch(next) is like connecting our error handling pipe to Express's main error system. Without it, errors would leak out and never reach our error handler.
Our error handling middleware processes all caught errors:
app.use((err, req, res, next) => {
console.error(err); // Log error for debugging
res.status(400); // Set appropriate status
res.json({ // Send formatted response
error: err.message,
});
});
In Express applications, we typically encounter several types of asynchronous operations:
1. Database queries
2. External API calls
3. File system operations
4. Authentication processes
// Example of comprehensive error handling
app.get('/api/data', async (req, res, next) => {
try {
const data = await fetchData();
if (!data) {
throw new Error('Data not found');
}
res.json(data);
} catch (err) {
// Add custom properties if needed
err.statusCode = err.statusCode || 500;
err.customProperty = 'Additional context';
next(err);
}
});
To verify your error handling is working correctly, test these scenarios:
// Test successful cases
curl http://localhost:5000/wait1sec // Should succeed ~50% of the time
// Test error cases
curl http://localhost:5000/wait4me // Should fail ~50% of the time
// Verify responses
Success: {"message":"Wait complete"}
Error: {"error":"A delay error has occurred!"}
For production applications, consider implementing:
// Custom error classes
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
this.statusCode = 400;
}
}
// Error handler with different responses based on error type
app.use((err, req, res, next) => {
if (err instanceof ValidationError) {
res.status(err.statusCode).json({
type: 'validation_error',
message: err.message
});
} else {
res.status(500).json({
type: 'server_error',
message: 'An unexpected error occurred'
});
}
});
Remember: Good error handling is like having a reliable safety system - it should catch problems early, provide clear information about what went wrong, and ensure your application continues running smoothly even when errors occur.