告别Try-Catch:Express中的智能异步错误处理
当使用Node.js和Express构建后端时,我们很可能使用async/await来处理数据库查询或API调用等操作。
但有一个问题——如果我们不正确处理错误,我们的服务器可能会崩溃或表现不可预测。😬
在这篇文章中,你将学习在Express中处理异步错误的清洁、可扩展的方法:
为什么在每个路由中使用try-catch很痛苦
如何用可重用的asyncHandler()修复它
如何使用外部库简化这个过程
如何使用我自己的包:express-error-toolkit
如何定义自定义错误类
以及如何设置全局错误处理器
🚨 到处使用Try-Catch的问题
这是我们通常处理错误的方式:
- app.get('/api/users/:id', async (req, res, next) => {
- try {
- const user = await User.findById(req.params.id);
- if (!user) return res.status(404).json({ message: 'User not found' });
- res.json(user);
- } catch (error) {
- next(error);
- }
- });
在每个路由中重复这样做是:
冗余的
丑陋的
容易忘记
让我们修复这个问题。
✅ 选项1:编写自定义asyncHandler
- // utils/asyncHandler.js
- const asyncHandler = (fn) => {
- return (req, res, next) => {
- Promise.resolve(fn(req, res, next)).catch(next);
- };
- };
- module.exports = asyncHandler;
像这样使用它:
- const asyncHandler = require('../utils/asyncHandler');
- app.get('/api/users/:id', asyncHandler(async (req, res) => {
- const user = await User.findById(req.params.id);
- if (!user) throw new Error('User not found');
- res.json(user);
- }));
清洁。可重用。没有try-catch。
📦 选项2:使用库(强烈推荐)
🔹 express-error-toolkit — 在npm上查看
我构建了这个包来使Express应用中的错误处理变得更加容易。它包括:
一个asyncHandler()函数
预定义的错误类(NotFoundError、BadRequestError等)
一个全局错误处理中间件
开发环境中的清洁堆栈跟踪
安装
- npm install express-error-toolkit
使用
- const { asyncHandler } = require('express-error-toolkit');
- app.get('/api/users/:id', asyncHandler(async (req, res) => {
- const user = await User.findById(req.params.id);
- if (!user) throw new Error('User not found');
- res.json(user);
- }));
🧱 定义自定义错误类
如果你不使用包,你可以定义自己的:
- // utils/ApiError.js
- class ApiError extends Error {
- constructor(statusCode, message) {
- super(message);
- this.statusCode = statusCode;
- Error.captureStackTrace(this, this.constructor);
- }
- }
- module.exports = ApiError;
使用:
- const ApiError = require('../utils/ApiError');
- if (!user) throw new ApiError(404, 'User not found');
或者使用express-error-toolkit的内置错误
- const { NotFoundError } = require('express-error-toolkit');
- if (!user) throw new NotFoundError('User not found');
🌍 全局错误处理中间件
在你的中间件链的末尾添加这个:
- app.use((err, req, res, next) => {
- const status = err.statusCode || 500;
- const message = err.message || 'Internal Server Error';
- res.status(status).json({
- success: false,
- message,
- stack: process.env.NODE_ENV === 'production' ? null : err.stack,
- });
- });
或者使用express-error-toolkit的内置处理器:
- const { globalErrorHandler } = require('express-error-toolkit');
- app.use(globalErrorHandler);
🧪 完整示例
- const express = require('express');
- const mongoose = require('mongoose');
- const {
- NotFoundError,
- asyncHandler,
- globalErrorHandler,
- } = require('express-error-toolkit');
- const app = express();
- app.use(express.json());
- app.get('/api/users/:id', asyncHandler(async (req, res) => {
- const user = await User.findById(req.params.id);
- if (!user) throw new NotFoundError('User not found');
- res.json(user);
- }));
- app.use(globalErrorHandler);
- app.listen(3000, () => console.log('Server running on port 3000'));
🧠 最终思考
✅ 使用asyncHandler避免在每个路由中使用try-catch
📦 使用express-error-toolkit获得功能完整、清洁的设置
🧱 使用自定义类抛出有意义的错误
🌍 在一个全局中间件中捕获和格式化所有错误
遵循这种方法,你的Express后端将是清洁、可扩展和生产就绪的。🚀
详细实现指南
1. 基础asyncHandler实现
- // utils/asyncHandler.js
- const asyncHandler = (fn) => {
- return (req, res, next) => {
- Promise.resolve(fn(req, res, next)).catch(next);
- };
- };
- module.exports = asyncHandler;
2. 高级asyncHandler(带错误类型检查)
- // utils/asyncHandler.js
- const asyncHandler = (fn) => {
- return (req, res, next) => {
- Promise.resolve(fn(req, res, next))
- .catch((error) => {
- // 检查是否是已知的API错误
- if (error.statusCode) {
- return next(error);
- }
- // 处理未知错误
- console.error('Unexpected error:', error);
- next(new Error('Internal Server Error'));
- });
- };
- };
- module.exports = asyncHandler;
3. 完整的错误类系统
- // utils/errors.js
- // 基础API错误类
- class ApiError extends Error {
- constructor(statusCode, message, isOperational = true) {
- super(message);
- this.statusCode = statusCode;
- this.isOperational = isOperational;
- Error.captureStackTrace(this, this.constructor);
- }
- }
- // 特定错误类
- class NotFoundError extends ApiError {
- constructor(message = 'Resource not found') {
- super(404, message);
- }
- }
- class BadRequestError extends ApiError {
- constructor(message = 'Bad request') {
- super(400, message);
- }
- }
- class UnauthorizedError extends ApiError {
- constructor(message = 'Unauthorized') {
- super(401, message);
- }
- }
- class ForbiddenError extends ApiError {
- constructor(message = 'Forbidden') {
- super(403, message);
- }
- }
- class ValidationError extends ApiError {
- constructor(message = 'Validation failed') {
- super(422, message);
- }
- }
- class ConflictError extends ApiError {
- constructor(message = 'Conflict') {
- super(409, message);
- }
- }
- module.exports = {
- ApiError,
- NotFoundError,
- BadRequestError,
- UnauthorizedError,
- ForbiddenError,
- ValidationError,
- ConflictError,
- };
4. 高级全局错误处理器
- // middleware/errorHandler.js
- const { ApiError } = require('../utils/errors');
- const errorHandler = (err, req, res, next) => {
- let error = { ...err };
- error.message = err.message;
- // 记录错误
- console.error(err);
- // Mongoose错误处理
- if (err.name === 'CastError') {
- const message = 'Resource not found';
- error = new ApiError(404, message);
- }
- // Mongoose重复键错误
- if (err.code === 11000) {
- const message = 'Duplicate field value entered';
- error = new ApiError(400, message);
- }
- // Mongoose验证错误
- if (err.name === 'ValidationError') {
- const message = Object.values(err.errors).map(val => val.message).join(', ');
- error = new ApiError(400, message);
- }
- // JWT错误
- if (err.name === 'JsonWebTokenError') {
- const message = 'Invalid token';
- error = new ApiError(401, message);
- }
- // JWT过期错误
- if (err.name === 'TokenExpiredError') {
- const message = 'Token expired';
- error = new ApiError(401, message);
- }
- res.status(error.statusCode || 500).json({
- success: false,
- error: error.message || 'Internal Server Error',
- ...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
- });
- };
- module.exports = errorHandler;
5. 完整的Express应用示例
- // app.js
- const express = require('express');
- const mongoose = require('mongoose');
- const asyncHandler = require('./utils/asyncHandler');
- const {
- NotFoundError,
- BadRequestError,
- ValidationError,
- } = require('./utils/errors');
- const errorHandler = require('./middleware/errorHandler');
- const app = express();
- // 中间件
- app.use(express.json());
- // 用户模型(示例)
- const User = mongoose.model('User', {
- name: String,
- email: String,
- age: Number,
- });
- // 路由示例
- app.get('/api/users', asyncHandler(async (req, res) => {
- const users = await User.find();
- res.json({
- success: true,
- data: users,
- });
- }));
- app.get('/api/users/:id', asyncHandler(async (req, res) => {
- const user = await User.findById(req.params.id);
- if (!user) {
- throw new NotFoundError('User not found');
- }
- res.json({
- success: true,
- data: user,
- });
- }));
- app.post('/api/users', asyncHandler(async (req, res) => {
- const { name, email, age } = req.body;
- // 验证
- if (!name || !email) {
- throw new ValidationError('Name and email are required');
- }
- if (age && (age < 0 || age > 150)) {
- throw new ValidationError('Age must be between 0 and 150');
- }
- const user = await User.create({ name, email, age });
- res.status(201).json({
- success: true,
- data: user,
- });
- }));
- app.put('/api/users/:id', asyncHandler(async (req, res) => {
- const { name, email, age } = req.body;
- const user = await User.findByIdAndUpdate(
- req.params.id,
- { name, email, age },
- { new: true, runValidators: true }
- );
- if (!user) {
- throw new NotFoundError('User not found');
- }
- res.json({
- success: true,
- data: user,
- });
- }));
- app.delete('/api/users/:id', asyncHandler(async (req, res) => {
- const user = await User.findByIdAndDelete(req.params.id);
- if (!user) {
- throw new NotFoundError('User not found');
- }
- res.json({
- success: true,
- message: 'User deleted successfully',
- });
- }));
- // 404处理
- app.use('*', (req, res) => {
- throw new NotFoundError(`Route ${req.originalUrl} not found`);
- });
- // 全局错误处理(必须在最后)
- app.use(errorHandler);
- // 启动服务器
- const PORT = process.env.PORT || 3000;
- app.listen(PORT, () => {
- console.log(`Server running on port ${PORT}`);
- });
6. 使用express-error-toolkit的完整示例
- // app-with-toolkit.js
- const express = require('express');
- const mongoose = require('mongoose');
- const {
- asyncHandler,
- NotFoundError,
- BadRequestError,
- ValidationError,
- globalErrorHandler,
- } = require('express-error-toolkit');
- const app = express();
- // 中间件
- app.use(express.json());
- // 用户模型
- const User = mongoose.model('User', {
- name: String,
- email: String,
- age: Number,
- });
- // 路由
- app.get('/api/users', asyncHandler(async (req, res) => {
- const users = await User.find();
- res.json({
- success: true,
- data: users,
- });
- }));
- app.get('/api/users/:id', asyncHandler(async (req, res) => {
- const user = await User.findById(req.params.id);
- if (!user) {
- throw new NotFoundError('User not found');
- }
- res.json({
- success: true,
- data: user,
- });
- }));
- app.post('/api/users', asyncHandler(async (req, res) => {
- const { name, email, age } = req.body;
- if (!name || !email) {
- throw new ValidationError('Name and email are required');
- }
- const user = await User.create({ name, email, age });
- res.status(201).json({
- success: true,
- data: user,
- });
- }));
- // 404处理
- app.use('*', () => {
- throw new NotFoundError('Route not found');
- });
- // 全局错误处理
- app.use(globalErrorHandler);
- // 启动服务器
- const PORT = process.env.PORT || 3000;
- app.listen(PORT, () => {
- console.log(`Server running on port ${PORT}`);
- });
7. 测试错误处理
- // test-error-handling.js
- const request = require('supertest');
- const app = require('./app');
- describe('Error Handling Tests', () => {
- test('should return 404 for non-existent user', async () => {
- const response = await request(app)
- .get('/api/users/nonexistentid')
- .expect(404);
- expect(response.body).toEqual({
- success: false,
- error: 'User not found',
- });
- });
- test('should return 400 for invalid user data', async () => {
- const response = await request(app)
- .post('/api/users')
- .send({ name: 'John' }) // 缺少email
- .expect(400);
- expect(response.body).toEqual({
- success: false,
- error: 'Name and email are required',
- });
- });
- test('should return 404 for non-existent route', async () => {
- const response = await request(app)
- .get('/api/nonexistent')
- .expect(404);
- expect(response.body).toEqual({
- success: false,
- error: 'Route /api/nonexistent not found',
- });
- });
- });
最佳实践总结
1. 使用asyncHandler
避免在每个路由中重复try-catch
保持代码清洁和可读
确保所有异步错误都被正确捕获
2. 定义有意义的错误类
使用HTTP状态码
提供清晰的错误消息
区分操作错误和系统错误
3. 实现全局错误处理
统一错误响应格式
在开发环境显示堆栈跟踪
在生产环境隐藏敏感信息
4. 使用库简化开发
express-error-toolkit提供完整解决方案
减少样板代码
提供预定义的错误类
5. 测试错误处理
确保错误响应格式正确
验证状态码和消息
测试边界情况
