Table Of Contents
Problem
Async functions in Express routes don't automatically catch errors, leading to unhandled promise rejections and server crashes when async operations fail.
Solution
const express = require('express');
const app = express();
app.use(express.json());
// 1. Async Wrapper Function (Recommended Approach)
const asyncHandler = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
// Using the async wrapper
app.get('/users/:id', asyncHandler(async (req, res) => {
const userId = req.params.id;
// Simulate async database call that might fail
const user = await getUserFromDatabase(userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ user });
}));
// 2. Try-Catch in Every Async Route (Manual Approach)
app.post('/users', async (req, res, next) => {
try {
const { name, email } = req.body;
// Async operations that might fail
await validateEmail(email);
const user = await createUser({ name, email });
await sendWelcomeEmail(email);
res.status(201).json({
message: 'User created successfully',
user
});
} catch (error) {
next(error); // Pass error to error handler
}
});
// 3. Express 5 Style (Native Async Support)
// Note: This works in Express 5+, for Express 4 use wrapper
app.get('/posts', async (req, res) => {
const posts = await getPostsFromDatabase();
res.json({ posts });
});
// 4. Multiple Async Operations with Error Handling
app.get('/user-dashboard/:id', asyncHandler(async (req, res) => {
const userId = req.params.id;
// Parallel async operations
const [user, posts, notifications] = await Promise.all([
getUserFromDatabase(userId),
getUserPosts(userId),
getUserNotifications(userId)
]);
res.json({
user,
posts,
notifications,
total: posts.length
});
}));
// 5. Conditional Async Operations
app.put('/users/:id', asyncHandler(async (req, res) => {
const userId = req.params.id;
const updates = req.body;
const existingUser = await getUserFromDatabase(userId);
if (!existingUser) {
return res.status(404).json({ error: 'User not found' });
}
// Only update email if it changed
if (updates.email && updates.email !== existingUser.email) {
await validateEmailUnique(updates.email);
}
const updatedUser = await updateUser(userId, updates);
// Send notification only if email changed
if (updates.email && updates.email !== existingUser.email) {
await sendEmailChangeNotification(updates.email);
}
res.json({
message: 'User updated successfully',
user: updatedUser
});
}));
// 6. Async Middleware
const authenticateAsync = asyncHandler(async (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
const user = await verifyToken(token);
req.user = user;
next();
});
app.get('/protected', authenticateAsync, asyncHandler(async (req, res) => {
const userData = await getProtectedUserData(req.user.id);
res.json({ userData });
}));
// 7. File Operations with Async Error Handling
const fs = require('fs').promises;
const path = require('path');
app.post('/upload-process', asyncHandler(async (req, res) => {
const { filename, content } = req.body;
const filePath = path.join(__dirname, 'uploads', filename);
// Ensure directory exists
await fs.mkdir(path.dirname(filePath), { recursive: true });
// Write file
await fs.writeFile(filePath, content, 'utf8');
// Process file
const processedContent = await processFile(filePath);
res.json({
message: 'File uploaded and processed',
filename,
processed: processedContent
});
}));
// 8. Database Transaction with Error Handling
app.post('/transfer', asyncHandler(async (req, res) => {
const { fromAccount, toAccount, amount } = req.body;
// Start transaction
const transaction = await startTransaction();
try {
await debitAccount(fromAccount, amount, transaction);
await creditAccount(toAccount, amount, transaction);
await logTransfer(fromAccount, toAccount, amount, transaction);
await commitTransaction(transaction);
res.json({
message: 'Transfer completed successfully',
from: fromAccount,
to: toAccount,
amount
});
} catch (error) {
await rollbackTransaction(transaction);
throw error; // Re-throw to be caught by asyncHandler
}
}));
// 9. Centralized Error Handler
app.use((err, req, res, next) => {
console.error('Error:', err.message);
console.error('Stack:', err.stack);
// Handle specific error types
if (err.name === 'ValidationError') {
return res.status(400).json({
error: 'Validation Error',
message: err.message,
details: err.details
});
}
if (err.name === 'UnauthorizedError') {
return res.status(401).json({
error: 'Unauthorized',
message: 'Invalid or expired token'
});
}
if (err.code === 'ENOENT') {
return res.status(404).json({
error: 'File Not Found',
message: 'The requested file could not be found'
});
}
// Default error response
res.status(500).json({
error: 'Internal Server Error',
message: process.env.NODE_ENV === 'production'
? 'Something went wrong'
: err.message
});
});
// Mock async functions (replace with real implementations)
async function getUserFromDatabase(id) {
if (id === '999') throw new Error('Database connection failed');
return { id, name: 'John Doe', email: 'john@example.com' };
}
async function createUser(userData) {
if (!userData.email) throw new Error('Email is required');
return { id: Date.now(), ...userData };
}
async function validateEmail(email) {
if (!email.includes('@')) {
throw new Error('Invalid email format');
}
}
async function sendWelcomeEmail(email) {
// Simulate email service that might fail
if (Math.random() > 0.8) {
throw new Error('Email service unavailable');
}
}
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Explanation
Express doesn't automatically catch errors in async functions, causing unhandled promise rejections. The asyncHandler
wrapper catches any rejected promises and passes them to Express error middleware using next(error)
.
Always use try-catch blocks or wrapper functions for async routes. The centralized error handler processes all caught errors, allowing you to handle different error types consistently and return appropriate HTTP status codes and messages.
Share this article
Add Comment
No comments yet. Be the first to comment!