Navigation

Node.js

How to Handle Async Errors in Express Routes

Properly handle async/await errors in Express.js routes. Prevent unhandled promise rejections and create centralized error handling for async operations.

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!

More from Node.js