Express错误处理机制
1. 错误处理基础
在Express中,错误处理是通过特殊的中间件函数实现的。这些函数有四个参数(err, req, res, next),其中第一个参数是错误对象。
错误处理是任何健壮Web应用程序的关键组成部分,一个良好的错误处理系统可以:
- 提供有用的错误信息给开发人员进行调试
- 向用户显示友好的错误消息
- 防止敏感信息泄露
- 确保应用程序在遇到错误时能够优雅地失败
2. 内置错误处理
Express有一个内置的错误处理程序,它会捕获并处理应用程序中可能出现的任何错误。这个默认的错误处理中间件函数添加在中间件函数堆栈的末尾。
当中间件调用next(err)时,Express会跳过所有剩余的非错误处理中间件函数,直接进入错误处理中间件。
3. 自定义错误处理中间件
可以通过定义错误处理中间件函数来覆盖默认的错误处理行为:
app.use((err, req, res, next) => {
console.error(err.stack)
res.status(500).send('Something broke!')
})
重要提示:错误处理中间件函数必须有四个参数(err, req, res, next)。即使不需要使用next参数,也必须在函数签名中包含它,否则Express会将其视为常规中间件函数,不会捕获错误。
4. 错误处理中间件的放置
错误处理中间件应该定义在应用程序中的所有其他路由和中间件之后:
const express = require('express')
const app = express()
// 应用级中间件
app.use(express.json())
// 路由
app.get('/', (req, res) => {
res.send('Hello World!')
})
// 路由错误处理
app.use((req, res, next) => {
const err = new Error('Not Found')
err.status = 404
next(err)
})
// 错误处理中间件
app.use((err, req, res, next) => {
res.status(err.status || 500)
res.send({
error: {
status: err.status || 500,
message: err.message
}
})
})
5. 错误处理流程
当Express应用程序中发生错误时,错误处理流程如下:
- 发生错误(可能是同步操作抛出的错误或通过
next(err)传递的错误) - Express跳过所有剩余的非错误处理中间件
- 按定义顺序执行所有错误处理中间件
- 如果没有自定义错误处理中间件,则使用Express的默认错误处理程序
6. 处理同步和异步错误
6.1 同步错误处理
对于同步代码,Express会自动捕获抛出的错误:
app.get('/synchronous-error', (req, res) => {
throw new Error('This is a synchronous error')
})
6.2 异步错误处理
对于异步代码,需要显式地将错误传递给next()函数:
回调式异步错误处理
app.get('/asynchronous-error', (req, res, next) => {
fs.readFile('/file-does-not-exist', (err, data) => {
if (err) {
next(err) // 显式传递错误给next()
} else {
res.send(data)
}
})
})
Promise式异步错误处理
对于返回Promise的异步操作,可以使用.catch()来捕获错误并传递给next():
app.get('/promise-error', (req, res, next) => {
someAsyncOperation()
.then(result => res.send(result))
.catch(err => next(err)) // 传递Promise拒绝给next()
})
Async/Await错误处理
对于使用async/await的异步操作,可以使用try/catch来捕获错误:
app.get('/async-await-error', async (req, res, next) => {
try {
const result = await someAsyncOperation()
res.send(result)
} catch (err) {
next(err) // 传递捕获的错误给next()
}
})
7. 创建错误处理中间件工具
为了简化异步错误处理,可以创建一个工具函数来包装async/await函数:
// 错误处理工具
const asyncHandler = fn => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next))
.catch(next)
}
}
// 使用示例
app.get('/users', asyncHandler(async (req, res) => {
const users = await User.find({})
res.json(users)
}))
8. 错误类型和错误对象
在Express中,可以通过扩展JavaScript的Error对象来创建自定义错误类型:
// 创建自定义错误类
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('/users/:id', asyncHandler(async (req, res, next) => {
const user = await User.findById(req.params.id)
if (!user) {
return next(new AppError('No user found with that ID', 404))
}
res.json(user)
}))
9. 不同环境的错误处理
在开发环境和生产环境中,错误处理的需求通常不同:
- 开发环境:需要详细的错误信息以便调试
- 生产环境:需要简洁的错误信息,避免泄露敏感数据
const sendErrorDev = (err, req, res) => {
res.status(err.statusCode).json({
status: err.status,
error: err,
message: err.message,
stack: err.stack
})
}
const sendErrorProd = (err, req, res) => {
// 对于可操作的错误,向客户端发送友好的错误消息
if (err.isOperational) {
return res.status(err.statusCode).json({
status: err.status,
message: err.message
})
}
// 对于编程或其他未知错误,不向客户端泄露错误细节
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, req, res)
} else if (process.env.NODE_ENV === 'production') {
sendErrorProd(err, req, res)
}
})
10. 集中式错误处理
对于大型应用程序,可以将错误处理逻辑集中到一个单独的文件中:
10.1 创建错误控制器
controllers/errorController.js:
const AppError = require('./../utils/appError')
const handleCastErrorDB = err => {
const message = `Invalid ${err.path}: ${err.value}.`
return new AppError(message, 400)
}
const handleDuplicateFieldsDB = err => {
const value = err.errmsg.match(/".*?"/)[0]
const message = `Duplicate field value: ${value}. Please use another value!`
return new AppError(message, 400)
}
const handleValidationErrorDB = err => {
const errors = Object.values(err.errors).map(el => el.message)
const message = `Invalid input data. ${errors.join('. ')}`
return new AppError(message, 400)
}
const sendErrorDev = (err, req, res) => {
return res.status(err.statusCode).json({
status: err.status,
error: err,
message: err.message,
stack: err.stack
})
}
const sendErrorProd = (err, req, res) => {
if (err.isOperational) {
return res.status(err.statusCode).json({
status: err.status,
message: err.message
})
}
console.error('ERROR 💥', err)
return res.status(500).json({
status: 'error',
message: 'Something went very wrong!'
})
}
module.exports = (err, req, res, next) => {
err.statusCode = err.statusCode || 500
err.status = err.status || 'error'
if (process.env.NODE_ENV === 'development') {
sendErrorDev(err, req, res)
} else if (process.env.NODE_ENV === 'production') {
let error = { ...err }
error.message = err.message
if (error.name === 'CastError') error = handleCastErrorDB(error)
if (error.code === 11000) error = handleDuplicateFieldsDB(error)
if (error.name === 'ValidationError') error = handleValidationErrorDB(error)
sendErrorProd(error, req, res)
}
}
10.2 在主应用程序中使用错误控制器
app.js:
const express = require('express')
const app = express()
const globalErrorHandler = require('./controllers/errorController')
// ... 其他中间件和路由 ...
// 全局错误处理中间件
app.use(globalErrorHandler)
module.exports = app
11. 常见错误处理模式
11.1 404错误处理
捕获所有未匹配的路由并返回404错误:
// 放在所有路由之后
app.all('*', (req, res, next) => {
next(new AppError(`Can't find ${req.originalUrl} on this server!`, 404))
})
11.2 数据库错误处理
处理常见的数据库错误:
const handleDBError = (err, req, res, next) => {
if (err.code === 'ECONNREFUSED') {
return res.status(503).json({
status: 'error',
message: 'Database connection failed'
})
}
next(err)
}
app.use(handleDBError)
11.3 API错误响应格式统一
确保所有API错误响应具有一致的格式:
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500
res.status(statusCode).json({
success: false,
error: {
code: statusCode,
message: err.message || 'Internal Server Error',
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
}
})
})
12. 错误处理最佳实践
- 使用适当的HTTP状态码:根据错误类型选择正确的状态码
- 区分操作错误和编程错误:对于可预见的错误(如验证失败)和不可预见的错误(如代码错误)采用不同的处理策略
- 不在生产环境中泄露敏感信息:避免在生产环境中向客户端显示堆栈跟踪或其他敏感信息
- 记录错误日志:确保所有错误都被记录,以便进行调试和监控
- 使用错误类层次结构:创建自定义错误类以表示不同类型的错误
- 统一错误响应格式:确保所有API错误响应具有一致的格式
- 集中式错误处理:将错误处理逻辑集中到一个或几个地方
- 使用异步错误处理工具:使用工具函数简化异步错误处理
- 编写错误测试:为错误处理逻辑编写测试,确保其按预期工作
- 监控错误率:设置监控以跟踪应用程序中的错误率和类型
13. 练习项目
创建一个具有完整错误处理系统的Express应用程序,包括:
- 自定义错误类
- 针对开发和生产环境的不同错误处理逻辑
- 集中式错误处理控制器
- 特定类型错误的处理函数
- 统一的错误响应格式
- 404路由处理
- 异步错误处理工具
通过这个练习,你将掌握Express错误处理的高级技术,能够构建更加健壮和可靠的Web应用程序。