告别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);
}
});
javascript
在每个路由中重复这样做是:
冗余的
丑陋的
容易忘记
让我们修复这个问题。
选项1:编写自定义asyncHandler
// utils/asyncHandler.js
const asyncHandler = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};

module.exports = asyncHandler;
javascript
像这样使用它:
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);
}));
javascript
清洁。可重用。没有try-catch
📦 选项2:使用库(强烈推荐)
🔹 express-error-toolkit 在npm上查看
我构建了这个包来使Express应用中的错误处理变得更加容易。它包括:
一个asyncHandler()函数
预定义的错误类(NotFoundErrorBadRequestError等)
一个全局错误处理中间件
开发环境中的清洁堆栈跟踪
安装
npm install express-error-toolkit
bash
使用
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);
}));
javascript
🧱 定义自定义错误类
如果你不使用包,你可以定义自己的:
// utils/ApiError.js
class ApiError extends Error {
constructor(statusCode, message) {
super(message);
this.statusCode = statusCode;
Error.captureStackTrace(this, this.constructor);
}
}

module.exports = ApiError;
javascript
使用:
const ApiError = require('../utils/ApiError');

if (!user) throw new ApiError(404, 'User not found');
javascript
或者使用express-error-toolkit的内置错误
const { NotFoundError } = require('express-error-toolkit');

if (!user) throw new NotFoundError('User not found');
javascript
🌍 全局错误处理中间件
在你的中间件链的末尾添加这个:
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,
});
});
javascript
或者使用express-error-toolkit的内置处理器:
const { globalErrorHandler } = require('express-error-toolkit');

app.use(globalErrorHandler);
javascript
🧪 完整示例
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'));
javascript
🧠 最终思考
使用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;
javascript
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;
javascript
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,
};
javascript
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;
javascript
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}`);
});
javascript
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}`);
});
javascript
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',
});
});
});
javascript
最佳实践总结
1. 使用asyncHandler
避免在每个路由中重复try-catch
保持代码清洁和可读
确保所有异步错误都被正确捕获
2. 定义有意义的错误类
使用HTTP状态码
提供清晰的错误消息
区分操作错误和系统错误
3. 实现全局错误处理
统一错误响应格式
在开发环境显示堆栈跟踪
在生产环境隐藏敏感信息
4. 使用库简化开发
express-error-toolkit提供完整解决方案
减少样板代码
提供预定义的错误类
5. 测试错误处理
确保错误响应格式正确
验证状态码和消息
测试边界情况
Aa