您现在的位置是:网站首页 > 错误处理机制与调试文章详情
错误处理机制与调试
陈川
【
Node.js
】
45544人已围观
12999字
错误处理机制的基本概念
Express框架中的错误处理机制是构建健壮后端服务的关键部分。错误处理不仅仅是捕获异常,更是一套完整的流程控制策略。在Node.js的异步环境中,错误可能出现在任何环节,包括路由处理、中间件执行、数据库操作等场景。
传统的try-catch块在异步代码中往往失效,比如以下代码就无法捕获setTimeout中的错误:
try {
setTimeout(() => {
throw new Error('异步错误');
}, 100);
} catch (err) {
console.log('这里捕获不到错误'); // 不会执行
}
Express中的错误处理中间件
Express通过特殊形式的中间件处理错误,这类中间件需要接收四个参数:(err, req, res, next)。当调用next(err)或抛出异常时,Express会跳过所有常规中间件,直接进入错误处理流程。
基本错误处理中间件示例:
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('服务器内部错误!');
});
更完整的实现应该包含错误分类处理:
app.use((err, req, res, next) => {
if (err instanceof DatabaseError) {
res.status(503).json({ error: '数据库服务不可用' });
} else if (err instanceof AuthError) {
res.status(401).json({ error: '认证失败' });
} else {
res.status(err.status || 500).json({
error: err.message || '未知错误'
});
}
});
异步错误的特殊处理
在async/await函数中,错误处理需要特别注意。未捕获的Promise拒绝会导致整个应用崩溃。以下是常见错误模式:
// 危险写法:未处理Promise拒绝
app.get('/user', async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user);
});
// 正确写法:使用try-catch
app.get('/user', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
res.json(user);
} catch (err) {
next(err); // 将错误传递给错误处理中间件
}
});
Express 5.x将自动处理async函数中的拒绝,但在4.x版本中需要手动包装:
const asyncHandler = fn => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
app.get('/user', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user);
}));
自定义错误类型
创建特定错误类型可以提升错误处理的精确度:
class ApiError extends Error {
constructor(message, statusCode = 500) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
// 使用示例
app.get('/admin', (req, res, next) => {
if (!req.user.isAdmin) {
throw new ApiError('权限不足', 403);
}
// ...
});
日志记录策略
完善的错误处理必须包含日志记录。Winston或Morgan等库可以创建分级别、分文件的日志系统:
const winston = require('winston');
const { combine, timestamp, printf } = winston.format;
const errorFormat = printf(({ level, message, timestamp, stack }) => {
return `${timestamp} ${level}: ${message} - ${stack || '无堆栈信息'}`;
});
const logger = winston.createLogger({
level: 'error',
format: combine(
timestamp(),
errorFormat
),
transports: [
new winston.transports.File({ filename: 'error.log' })
]
});
// 在错误处理中间件中使用
app.use((err, req, res, next) => {
logger.error({
message: err.message,
stack: err.stack
});
next(err);
});
开发环境与生产环境的差异
错误响应在不同环境应有不同表现。开发环境需要详细错误信息:
app.use((err, req, res, next) => {
if (process.env.NODE_ENV === 'development') {
res.status(err.status || 500).json({
error: {
message: err.message,
stack: err.stack,
details: err.details
}
});
} else {
res.status(err.status || 500).json({
error: {
message: err.message
}
});
}
});
HTTP状态码的正确使用
常见状态码应用场景:
- 400 Bad Request:请求参数错误
- 401 Unauthorized:未认证
- 403 Forbidden:无权限
- 404 Not Found:资源不存在
- 422 Unprocessable Entity:语义错误
- 500 Internal Server Error:服务器内部错误
示例实现:
app.post('/login', (req, res, next) => {
if (!req.body.email || !req.body.password) {
throw new ApiError('邮箱和密码必填', 400);
}
const user = users.find(u => u.email === req.body.email);
if (!user) {
throw new ApiError('用户不存在', 404);
}
if (user.password !== req.body.password) {
throw new ApiError('密码错误', 401);
}
// ...
});
调试技巧与实践
Node.js调试有多种方式:
- 使用console.log(最简单但不够专业)
- Node.js内置调试器:
node inspect app.js
- Chrome DevTools调试:
node --inspect app.js
- VS Code调试配置:
{ "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "启动程序", "skipFiles": ["<node_internals>/**"], "program": "${workspaceFolder}/app.js" } ] }
调试中间件流程示例:
app.use((req, res, next) => {
console.log('请求到达时间:', new Date());
console.log('请求方法:', req.method);
console.log('请求路径:', req.path);
next();
});
app.get('/debug', (req, res, next) => {
debugger; // 在这里设置断点
const result = complexCalculation();
res.json({ result });
});
请求验证与错误预防
使用Joi等验证库可以在请求进入业务逻辑前拦截错误:
const Joi = require('joi');
const userSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')),
email: Joi.string().email({ minDomainSegments: 2 })
});
app.post('/register', (req, res, next) => {
const { error } = userSchema.validate(req.body);
if (error) {
throw new ApiError(error.details[0].message, 400);
}
// 继续处理...
});
数据库错误处理
数据库操作需要特殊错误处理策略:
app.get('/products', async (req, res, next) => {
try {
const products = await Product.find()
.skip(req.query.offset)
.limit(req.query.limit);
if (!products.length) {
throw new ApiError('未找到产品', 404);
}
res.json(products);
} catch (err) {
if (err.name === 'CastError') {
next(new ApiError('无效的查询参数', 400));
} else if (err.name === 'MongoNetworkError') {
next(new ApiError('数据库连接失败', 503));
} else {
next(err);
}
}
});
性能监控与错误关联
将错误与性能数据关联可以帮助定位问题根源:
const onHeaders = require('on-headers');
app.use((req, res, next) => {
const start = process.hrtime();
onHeaders(res, () => {
const duration = process.hrtime(start);
const ms = duration[0] * 1e3 + duration[1] * 1e-6;
req.requestDuration = ms;
});
next();
});
app.use((err, req, res, next) => {
if (req.requestDuration > 1000) {
err.slowRequest = true;
}
next(err);
});
客户端错误报告
前端JavaScript错误可以收集到后端:
app.post('/client-error', express.json(), (req, res) => {
const { message, stack, url, userAgent } = req.body;
ClientError.create({
message,
stack,
url,
userAgent,
timestamp: new Date()
});
res.sendStatus(200);
});
前端对应代码:
window.addEventListener('error', (event) => {
fetch('/client-error', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: event.message,
stack: event.error.stack,
url: window.location.href,
userAgent: navigator.userAgent
})
});
});
测试中的错误模拟
单元测试需要模拟各种错误场景:
const request = require('supertest');
describe('错误处理测试', () => {
it('应处理404错误', async () => {
const res = await request(app)
.get('/nonexistent-route')
.expect(404);
expect(res.body.error).toBe('未找到路由');
});
it('应处理数据库错误', async () => {
jest.spyOn(User, 'findById').mockRejectedValue(new Error('DB故障'));
const res = await request(app)
.get('/user/123')
.expect(500);
expect(res.body.error).toMatch(/数据库错误/);
});
});
进程级别的错误处理
即使有完善的错误处理,未捕获的异常仍可能导致进程退出:
process.on('uncaughtException', (err) => {
console.error('未捕获异常:', err);
// 记录错误后优雅退出
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的Promise拒绝:', reason);
// 可以在这里记录错误并继续运行
});
错误信息的国际化
多语言应用中的错误消息处理:
const locales = {
en: {
'auth.failed': 'Authentication failed',
'user.not_found': 'User not found'
},
zh: {
'auth.failed': '认证失败',
'user.not_found': '用户不存在'
}
};
class I18nError extends ApiError {
constructor(key, lang = 'en', status = 400) {
const message = locales[lang]?.[key] || key;
super(message, status);
this.key = key;
}
}
// 使用示例
app.get('/profile', (req, res, next) => {
const lang = req.acceptsLanguages('zh', 'en') || 'en';
if (!req.user) {
throw new I18nError('auth.failed', lang, 401);
}
// ...
});
安全考虑
错误处理中的安全注意事项:
- 不要在生产环境暴露堆栈跟踪
- 数据库错误可能包含敏感信息,需要过滤
- 错误消息不应透露系统内部细节
安全错误处理示例:
app.use((err, req, res, next) => {
// 过滤敏感信息
const safeError = {
message: err.message,
statusCode: err.statusCode
};
if (process.env.NODE_ENV === 'development') {
safeError.stack = err.stack;
}
// 记录完整错误
logger.error(err);
res.status(safeError.statusCode || 500).json({
error: safeError
});
});
微服务架构中的错误传播
在微服务环境中,错误可能需要跨服务传递:
app.get('/order/:id', async (req, res, next) => {
try {
const order = await OrderService.get(req.params.id);
const user = await UserService.get(order.userId);
res.json({ ...order, user });
} catch (err) {
if (err.isRemoteError) {
// 来自远程服务的错误
next(new ApiError(err.remoteMessage, err.remoteStatus));
} else {
next(err);
}
}
});
错误重试策略
对于暂时性错误,可以实现自动重试机制:
const retry = async (fn, retries = 3, delay = 100) => {
try {
return await fn();
} catch (err) {
if (retries <= 0 || !err.isRetryable) {
throw err;
}
await new Promise(res => setTimeout(res, delay));
return retry(fn, retries - 1, delay * 2);
}
};
app.get('/data', async (req, res, next) => {
try {
const data = await retry(() => fetchExternalApi());
res.json(data);
} catch (err) {
next(err);
}
});
错误分类与监控
将错误分类有助于监控系统报警:
class ErrorCategory {
static get DATABASE() { return 'database'; }
static get AUTH() { return 'auth'; }
static get VALIDATION() { return 'validation'; }
// ...
}
app.use((err, req, res, next) => {
if (err instanceof DatabaseError) {
err.category = ErrorCategory.DATABASE;
} else if (err instanceof AuthError) {
err.category = ErrorCategory.AUTH;
}
next(err);
});
// 监控系统可以按分类统计错误
前端显示错误的最佳实践
后端错误如何在前端优雅显示:
// 后端返回结构
{
"error": {
"message": "操作失败",
"code": "INVALID_INPUT",
"details": [
{
"field": "email",
"message": "必须是有效的邮箱地址"
}
]
}
}
// 前端处理
function displayError(error) {
if (error.details) {
error.details.forEach(detail => {
const field = document.getElementById(detail.field);
if (field) {
field.nextElementSibling.textContent = detail.message;
}
});
} else {
showToast(error.message);
}
}
错误处理中间件的顺序
中间件顺序对错误处理至关重要:
// 1. 记录请求
app.use(morgan('dev'));
// 2. 解析请求体
app.use(express.json());
// 3. 路由处理
app.use('/api', apiRouter);
// 4. 404处理(放在所有路由之后)
app.use((req, res, next) => {
next(new ApiError('未找到路由', 404));
});
// 5. 错误处理(放在最后)
app.use((err, req, res, next) => {
// 处理所有错误
});
自定义错误页面
针对不同状态码返回不同错误页面:
app.use((err, req, res, next) => {
const status = err.status || 500;
if (req.accepts('html')) {
res.status(status).render(`errors/${status}`, {
message: err.message,
status
});
} else {
res.status(status).json({
error: {
message: err.message,
status
}
});
}
});
对应视图模板示例(views/errors/404.ejs):
<!DOCTYPE html>
<html>
<head>
<title>404 页面不存在</title>
</head>
<body>
<h1>404 - <%= message %></h1>
<p>您访问的页面不存在,请检查URL是否正确</p>
</body>
</html>
性能与错误处理的平衡
过于详细的错误处理可能影响性能,需要权衡:
// 生产环境可能不需要记录每个错误的完整堆栈
app.use((err, req, res, next) => {
if (process.env.NODE_ENV === 'production') {
logger.error(err.message, {
path: req.path,
method: req.method,
status: err.status || 500
});
} else {
logger.error(err.stack);
}
next(err);
});
错误处理与事务管理
数据库操作中的错误需要正确处理事务:
app.post('/order', async (req, res, next) => {
const session = await mongoose.startSession();
session.startTransaction();
try {
const order = new Order(req.body);
await order.save({ session });
await User.updateOne(
{ _id: req.user.id },
{ $push: { orders: order._id } },
{ session }
);
await session.commitTransaction();
res.status(201).json(order);
} catch (err) {
await session.abortTransaction();
next(err);
} finally {
session.endSession();
}
});
第三方API错误处理
集成第三方服务时的错误处理模式:
const callThirdPartyAPI = async () => {
try {
const response = await axios.get('https://api.example.com/data');
return response.data;
} catch (err) {
if (err.response) {
// 请求已发出,服务器返回状态码非2xx
throw new ThirdPartyError(
`第三方API错误: ${err.response.status}`,
err.response.status
);
} else if (err.request) {
// 请求已发出但无响应
throw new ThirdPartyError('第三方API无响应', 504);
} else {
// 其他错误
throw new ThirdPartyError('调用第三方API失败', 500);
}
}
};
错误处理中间件的单元测试
测试错误处理中间件的示例:
describe('错误处理中间件', () => {
let mockReq, mockRes, mockNext;
上一篇: 静态文件服务与资源托管
下一篇: Cookie与Session管理