错误处理
本章节介绍 Express.js 中的错误处理机制和最佳实践。
错误处理中间件
Express 错误处理中间件有四个参数:(err, req, res, next)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: '服务器错误' });
});
基本错误处理
同步错误
app.get('/sync-error', (req, res) => {
throw new Error('同步错误');
});
app.use((err, req, res, next) => {
res.status(500).json({ error: err.message });
});
异步错误
app.get('/async-error', (req, res, next) => {
setTimeout(() => {
try {
throw new Error('异步错误');
} catch (err) {
next(err);
}
}, 100);
});
异步函数错误
app.get('/async-await', async (req, res, next) => {
try {
const data = await fetchData();
res.json(data);
} catch (err) {
next(err);
}
});
express-async-errors
自动处理异步错误:
npm install express-async-errors
require('express-async-errors');
app.get('/async', async (req, res) => {
const data = await fetchData();
res.json(data);
});
自定义错误类
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(message = '资源未找到') {
super(message, 404);
}
}
class ValidationError extends AppError {
constructor(message = '验证失败') {
super(message, 400);
}
}
class UnauthorizedError extends AppError {
constructor(message = '未授权') {
super(message, 401);
}
}
module.exports = { AppError, NotFoundError, ValidationError, UnauthorizedError };
全局错误处理
const { AppError } = require('./errors');
app.use((err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
if (process.env.NODE_ENV === 'development') {
res.status(err.statusCode).json({
status: err.status,
message: err.message,
stack: err.stack,
error: err
});
} else {
if (err.isOperational) {
res.status(err.statusCode).json({
status: err.status,
message: err.message
});
} else {
console.error('ERROR:', err);
res.status(500).json({
status: 'error',
message: '服务器内部错误'
});
}
}
});
404 处理
app.use((req, res, next) => {
next(new NotFoundError(`找不到 ${req.originalUrl}`));
});
错误处理最佳实践
错误处理包装函数
const catchAsync = (fn) => {
return (req, res, next) => {
fn(req, res, next).catch(next);
};
};
app.get('/users/:id', catchAsync(async (req, res, next) => {
const user = await User.findById(req.params.id);
if (!user) {
return next(new NotFoundError('用户不存在'));
}
res.json(user);
}));
错误处理中间件分离
const errorHandler = (err, req, res, next) => {
const statusCode = err.statusCode || 500;
const message = err.message || '服务器错误';
res.status(statusCode).json({
success: false,
error: message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
};
const notFound = (req, res, next) => {
next(new NotFoundError(`路由 ${req.originalUrl} 不存在`));
};
module.exports = { errorHandler, notFound };
使用示例
const { errorHandler, notFound } = require('./middleware/error');
app.use('/api', apiRoutes);
app.use(notFound);
app.use(errorHandler);
错误日志
const fs = require('fs');
const path = require('path');
const logError = (err) => {
const logPath = path.join(__dirname, '../logs/error.log');
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${err.stack}\n`;
fs.appendFile(logPath, logMessage, (fsErr) => {
if (fsErr) console.error('写入日志失败:', fsErr);
});
};
app.use((err, req, res, next) => {
logError(err);
res.status(err.statusCode || 500).json({
status: 'error',
message: err.message
});
});
常见错误场景
数据库错误
app.post('/users', catchAsync(async (req, res, next) => {
try {
const user = await User.create(req.body);
res.status(201).json(user);
} catch (err) {
if (err.code === 11000) {
return next(new ValidationError('邮箱已被注册'));
}
next(err);
}
}));
JWT 错误
const jwt = require('jsonwebtoken');
const verifyToken = (req, res, next) => {
try {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return next(new UnauthorizedError('请先登录'));
}
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch (err) {
if (err.name === 'JsonWebTokenError') {
return next(new UnauthorizedError('Token 无效'));
}
if (err.name === 'TokenExpiredError') {
return next(new UnauthorizedError('Token 已过期'));
}
next(err);
}
};