如何在Express Controller中实现DRY?重复Catch代码的中间件优化问询
Absolutely! This is such a common frustration when working with async/await in Express controllers—all those repetitive try/catch blocks just clutter up your code and make it harder to maintain. The good news is middleware is perfect for solving this, and it’s straightforward to set up.
Step 1: Create an Async Handler Wrapper
First, we’ll make a utility function that wraps your async controller functions and automatically catches errors, passing them to Express’s error-handling pipeline. This way, you never have to write a catch block in your controllers again.
// utils/asyncHandler.js const asyncHandler = (fn) => (req, res, next) => { // Wrap the async function in Promise.resolve to handle both resolved and rejected promises Promise.resolve(fn(req, res, next)).catch(next); }; module.exports = asyncHandler;
Step 2: Refactor Your Controllers
Now, instead of wrapping every controller in try/catch, just pass your async function through the asyncHandler:
Before (with duplicate catch blocks):
// controllers/userController.js exports.getUsers = async (req, res) => { try { const users = await User.find(); res.json(users); } catch (err) { res.status(500).json({ error: 'Failed to fetch users' }); } }; exports.getUserById = async (req, res) => { try { const user = await User.findById(req.params.id); if (!user) { return res.status(404).json({ error: 'User not found' }); } res.json(user); } catch (err) { res.status(500).json({ error: 'Failed to fetch user' }); } };
After (clean, no duplicate catches):
// controllers/userController.js const asyncHandler = require('../utils/asyncHandler'); const User = require('../models/User'); exports.getUsers = asyncHandler(async (req, res) => { const users = await User.find(); res.json(users); }); exports.getUserById = asyncHandler(async (req, res) => { const user = await User.findById(req.params.id); if (!user) { // You can still throw custom errors here if needed throw new Error('User not found'); } res.json(user); });
Step 3: Set Up a Global Error-Handling Middleware
Next, add a top-level middleware to handle all errors that get passed to next() (including the ones caught by our asyncHandler). This is where you’ll define your unified error response format.
Make sure this middleware is defined after all your routes and other middleware:
// app.js const express = require('express'); const app = express(); // ... your routes, other middleware ... // Global error handler app.use((err, req, res, next) => { // Default to 500 if no status code is set const statusCode = err.statusCode || 500; // Default error message const errorMessage = err.message || 'Internal Server Error'; // Send the standardized error response res.status(statusCode).json({ error: errorMessage, // Optional: Include stack trace only in development ...(process.env.NODE_ENV === 'development' && { stack: err.stack }) }); }); app.listen(3000, () => console.log('Server running on port 3000'));
Bonus: Custom Error Classes (For Better Control)
If you want to distinguish between operational errors (like "user not found") and system errors (like database connection issues), create a custom error class:
// utils/AppError.js class AppError extends Error { constructor(message, statusCode) { super(message); this.statusCode = statusCode; // Mark this as an operational error (not a random system error) this.isOperational = true; // Capture stack trace without including the error class itself Error.captureStackTrace(this, this.constructor); } } module.exports = AppError;
Then use it in your controllers:
exports.getUserById = asyncHandler(async (req, res) => { const user = await User.findById(req.params.id); if (!user) { throw new AppError('User not found', 404); } res.json(user); });
And update your global error handler to handle operational errors differently if needed:
app.use((err, req, res, next) => { if (err.isOperational) { res.status(err.statusCode).json({ error: err.message }); } else { // Log system errors for debugging console.error('ERROR:', err); res.status(500).json({ error: 'Something went wrong!' }); } });
Why This Works
Express doesn’t automatically catch errors from async functions unless you pass them to next(). Our asyncHandler wraps the async function, catches any rejected promises, and forwards the error to next(), which then triggers our global error handler. This eliminates all those repetitive catch blocks in your controllers while keeping error handling consistent across your app.
内容的提问来源于stack exchange,提问作者Jamie Aden

