Appearance
Express错误处理
错误处理是任何Web应用的重要组成部分。Express提供了内置的错误处理机制,同时允许开发者自定义错误处理逻辑。
Express错误处理概览
Express错误处理遵循以下原则:
- 错误处理中间件有四个参数:(err, req, res, next)
- 错误处理中间件应定义在其他app.use()和路由调用之后
- Express会捕获路由和中间件执行过程中的同步和异步错误
错误处理中间件
基本错误处理中间件
javascript
// 错误处理中间件必须有四个参数
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});
错误处理中间件的位置
javascript
const express = require('express');
const app = express();
// 普通中间件
app.use((req, res, next) => {
console.log(`${req.method} ${req.path}`);
next();
});
// 路由
app.get('/', (req, res) => {
res.send('Hello World');
});
// 错误处理中间件 - 必须在所有路由定义之后
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});
错误类型和处理
同步错误
javascript
app.get('/sync-error', (req, res) => {
// 同步错误会被自动捕获
throw new Error('This is a synchronous error');
});
异步错误
javascript
app.get('/async-error', (req, res, next) => {
// 异步错误需要显式调用next()
setTimeout(() => {
try {
throw new Error('This is an asynchronous error');
} catch (err) {
next(err); // 将错误传递给错误处理中间件
}
}, 100);
});
// 或者使用Promise
app.get('/promise-error', (req, res, next) => {
someAsyncFunction()
.then(result => {
res.json(result);
})
.catch(err => {
next(err); // 传递错误
});
});
异步函数错误处理
javascript
// 使用async/await的错误处理
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
app.get('/async-await-error', asyncHandler(async (req, res) => {
const data = await someAsyncOperation();
res.json(data);
}));
// 或者在函数内部处理错误
app.get('/async-await-error-2', async (req, res, next) => {
try {
const data = await someAsyncOperation();
res.json(data);
} catch (err) {
next(err);
}
});
自定义错误类
创建自定义错误
javascript
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);
}
}
// 使用自定义错误
app.get('/custom-error', (req, res, next) => {
const error = new AppError('Resource not found', 404);
next(error);
});
不同类型的错误
javascript
class ValidationError extends AppError {
constructor(message) {
super(message, 400);
this.type = 'validation_error';
}
}
class AuthenticationError extends AppError {
constructor(message) {
super(message, 401);
this.type = 'authentication_error';
}
}
class AuthorizationError extends AppError {
constructor(message) {
super(message, 403);
this.type = 'authorization_error';
}
}
// 在路由中使用
app.post('/login', (req, res, next) => {
if (!req.body.username || !req.body.password) {
return next(new ValidationError('Username and password are required'));
}
// 其他逻辑...
});
全局错误处理
开发环境错误处理
javascript
const sendErrorDev = (err, res) => {
res.status(err.statusCode).json({
status: err.status,
error: err,
message: err.message,
stack: err.stack
});
};
生产环境错误处理
javascript
const sendErrorProd = (err, res) => {
// 可操作错误:发送可理解的错误信息
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: 'Something went very wrong!'
});
}
};
// 主错误处理中间件
app.use((err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
if (process.env.NODE_ENV === 'development') {
sendErrorDev(err, res);
} else {
sendErrorProd(err, res);
}
});
特定错误处理
404错误处理
javascript
// 404错误处理 - 应放在所有路由之后
app.all('*', (req, res, next) => {
const err = new AppError(`Can't find ${req.originalUrl} on this server!`, 404);
next(err);
});
数据库错误处理
javascript
const handleDBError = (err) => {
if (err.name === 'CastError') {
const message = `Invalid ${err.path}: ${err.value}.`;
return new AppError(message, 400);
}
if (err.code === 11000) {
const value = err.errmsg.match(/(["'])(\\?.)*?\1/)[0];
const message = `Duplicate field value: ${value}. Please use another value!`;
return new AppError(message, 400);
}
if (err.name === 'ValidationError') {
const errors = Object.values(err.errors).map(el => el.message);
const message = `Invalid input data. ${errors.join('. ')}`;
return new AppError(message, 400);
}
return err;
};
// 在错误处理中间件中使用
app.use((err, req, res, next) => {
let error = { ...err };
error.message = err.message;
// 处理MongoDB错误
if (err.name === 'CastError') error = handleDBError(error);
res.status(error.statusCode || 500).json({
status: error.status || 'error',
message: error.message || 'Internal Server Error'
});
});
错误记录和监控
使用日志记录错误
javascript
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'express-app' },
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
// 在错误处理中间件中记录错误
app.use((err, req, res, next) => {
logger.error({
message: err.message,
stack: err.stack,
url: req.url,
method: req.method,
ip: req.ip,
userAgent: req.get('User-Agent')
});
res.status(err.statusCode || 500).json({
status: 'error',
message: err.message
});
});
未捕获异常和拒绝的Promise
全局错误处理
javascript
// 处理未捕获的异常
process.on('uncaughtException', (err) => {
console.log('UNCAUGHT EXCEPTION! 💥 Shutting down...');
console.log(err.name, err.message);
// 正常关闭服务器
process.exit(1);
});
// 处理未处理的Promise拒绝
process.on('unhandledRejection', (err) => {
console.log('UNHANDLED REJECTION! 💥 Shutting down...');
console.log(err.name, err.message);
// 正常关闭服务器
server.close(() => {
process.exit(1);
});
});
验证错误处理
使用Joi进行验证和错误处理
javascript
const Joi = require('joi');
const userSchema = Joi.object({
name: Joi.string().min(3).max(30).required(),
email: Joi.string().email().required(),
age: Joi.number().integer().min(0).max(120)
});
app.post('/users', (req, res, next) => {
const { error, value } = userSchema.validate(req.body);
if (error) {
const err = new AppError(`Validation Error: ${error.details[0].message}`, 400);
return next(err);
}
// 使用验证后的数据
req.validatedData = value;
next();
});
中间件错误处理
路由中间件错误
javascript
// 认证中间件错误处理
const authenticate = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return next(new AuthenticationError('Access token required'));
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
if (err.name === 'JsonWebTokenError') {
return next(new AuthenticationError('Invalid token'));
}
if (err.name === 'TokenExpiredError') {
return next(new AuthenticationError('Token expired'));
}
return next(err);
}
};
app.get('/profile', authenticate, (req, res) => {
res.json(req.user);
});
错误处理最佳实践
1. 统一错误响应格式
javascript
const errorResponse = (res, statusCode, message, errors = null) => {
const response = {
status: 'error',
message: message,
timestamp: new Date().toISOString(),
path: res.req.originalUrl
};
if (errors) {
response.errors = errors;
}
return res.status(statusCode).json(response);
};
// 使用统一错误响应
app.use((err, req, res, next) => {
errorResponse(res, err.statusCode || 500, err.message);
});
2. 环境特定错误处理
javascript
const errorController = {
sendErrorDev: (err, res) => {
res.status(err.statusCode).json({
status: err.status,
error: err,
message: err.message,
stack: err.stack
});
},
sendErrorProd: (err, res) => {
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: 'Something went wrong!'
});
}
}
};
app.use((err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
if (process.env.NODE_ENV === 'development') {
errorController.sendErrorDev(err, res);
} else {
errorController.sendErrorProd(err, res);
}
});
3. 错误分类处理
javascript
// 根据错误类型进行不同处理
const handleSpecificErrors = (err) => {
// 数据库连接错误
if (err.code === 'ECONNREFUSED') {
return new AppError('Database connection failed', 500);
}
// 超时错误
if (err.code === 'ETIMEDOUT') {
return new AppError('Request timeout', 408);
}
// 资源未找到
if (err.status === 404) {
return new AppError('Resource not found', 404);
}
return err;
};
通过正确的错误处理,可以提高应用的稳定性和用户体验,同时便于调试和监控应用的运行状态。