Express 错误处理
错误处理是 Express 应用的关键环节。完善的错误处理机制可以确保应用的稳定性、提供良好的用户体验,并方便开发调试。
错误处理架构
错误传播流程
code
路由处理函数
│
│ throw Error 或 next(error)
▼
┌─────────────────────────────────────────────┐
│ Express 错误传播 │
├─────────────────────────────────────────────┤
│ 1. 同步错误自动捕获 │
│ 2. 异步错误需 next(err) 传递 │
│ 3. 按顺序查找错误处理中间件 │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 错误处理中间件 │
│ (err, req, res, next) │
├─────────────────────────────────────────────┤
│ • 记录日志 │
│ • 标准化响应格式 │
│ • 区分环境返回详情 │
└─────────────────────────────────────────────┘
│
▼
客户端响应错误类型分类
| 类型 | 来源 | 捕获方式 | 示例 |
|---|---|---|---|
| 同步错误 | 同步代码 throw | Express 自动捕获 | throw new Error() |
| 异步错误 | Promise 拒绝 | 需 next(err) 或包装器 | await fn() 异常 |
| 操作错误 | 业务逻辑 | 自定义错误类 | 验证失败、资源未找到 |
| 程序错误 | 代码 Bug | 应修复而非处理 | 未定义变量 |
| 系统错误 | 运行环境 | 进程级处理 | 内存不足、网络断开 |
同步与异步错误
同步错误(自动捕获)
Express 会自动捕获同步代码中的错误:
javascript
// ✅ 同步错误 - Express 自动捕获
app.get("/sync-error", (req, res) => {
throw new Error("同步错误被自动捕获")
})
// ✅ 在中间件中抛出
app.use((req, res, next) => {
if (!req.headers.authorization) {
throw new Error("缺少认证头") // 自动传递给错误处理
}
next()
})异步错误(需手动传递)
Express 4.x 不会自动捕获异步错误,需要手动处理:
javascript
// ❌ 异步错误不会被捕获
app.get("/async-error", async (req, res) => {
const user = await User.findById(req.params.id) // 错误会导致进程崩溃
res.json(user)
})
// ✅ 方案一:try-catch + next(err)
app.get("/users/:id", async (req, res, next) => {
try {
const user = await User.findById(req.params.id)
if (!user) {
throw new Error("用户不存在")
}
res.json(user)
} catch (error) {
next(error) // 传递给错误处理中间件
}
})
// ✅ 方案二:异步包装器
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next)
}
app.get("/users/:id", asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id)
if (!user) throw new Error("用户不存在")
res.json(user)
}))
// ✅ 方案三:Promise catch
app.get("/users/:id", (req, res, next) => {
User.findById(req.params.id)
.then(user => {
if (!user) throw new Error("用户不存在")
res.json(user)
})
.catch(next)
})Express 5.0 异步错误支持
Express 5.0 自动处理异步函数中的错误:
javascript
// Express 5.0+ 自动捕获异步错误
app.get("/users/:id", async (req, res) => {
const user = await User.findById(req.params.id)
// 错误会自动传递给错误处理中间件
res.json(user)
})错误处理中间件
错误处理中间件有四个参数 (err, req, res, next),必须定义在所有中间件和路由之后。
基本结构
javascript
app.use((err, req, res, next) => {
console.error(err.stack)
res.status(500).send("服务器错误")
})完整错误处理中间件
javascript
// middleware/errorHandler.js
const errorHandler = (err, req, res, next) => {
// 1. 设置默认值
const status = err.status || err.statusCode || 500
const message = err.message || "服务器内部错误"
// 2. 根据环境决定响应内容
const isDev = process.env.NODE_ENV === "development"
// 3. 构建响应对象
const response = {
success: false,
error: {
message,
status,
...(isDev && {
stack: err.stack,
details: err.details || null
})
}
}
// 4. 记录日志
console.error(`[${new Date().toISOString()}] ${status} - ${message}`)
if (isDev) console.error(err.stack)
// 5. 发送响应
res.status(status).json(response)
}
module.exports = errorHandler使用方式
javascript
const express = require("express")
const app = express()
const errorHandler = require("./middleware/errorHandler")
// ... 中间件和路由 ...
// 404 处理(放在路由之后、错误处理之前)
app.use((req, res, next) => {
const error = new Error(`未找到资源: ${req.originalUrl}`)
error.status = 404
next(error)
})
// 错误处理中间件(必须最后)
app.use(errorHandler)多个错误处理中间件
javascript
// 专门处理 JSON 解析错误
app.use((err, req, res, next) => {
if (err instanceof SyntaxError && err.status === 400 && "body" in err) {
return res.status(400).json({
success: false,
error: { message: "JSON 格式错误", status: 400 }
})
}
next(err)
})
// 专门处理验证错误
app.use((err, req, res, next) => {
if (err.name === "ValidationError") {
return res.status(400).json({
success: false,
error: { message: err.message, status: 400 }
})
}
next(err)
})
// 通用错误处理
app.use((err, req, res, next) => {
res.status(err.status || 500).json({
success: false,
error: { message: err.message, status: err.status || 500 }
})
})自定义错误类
基础错误类
javascript
// errors/AppError.js
class AppError extends Error {
constructor(message, status = 500, code = null) {
super(message)
this.status = status
this.code = code
this.isOperational = true // 标记为可操作错误(非程序 Bug)
this.timestamp = new Date().toISOString()
Error.captureStackTrace(this, this.constructor)
}
toJSON() {
return {
message: this.message,
status: this.status,
code: this.code,
...(process.env.NODE_ENV === "development" && {
stack: this.stack
})
}
}
}
module.exports = AppError常用错误类型
javascript
// errors/index.js
const AppError = require("./AppError")
// 400 - 请求错误
class BadRequestError extends AppError {
constructor(message = "请求参数错误") {
super(message, 400, "BAD_REQUEST")
}
}
// 400 - 验证错误
class ValidationError extends AppError {
constructor(message = "数据验证失败", errors = []) {
super(message, 400, "VALIDATION_ERROR")
this.errors = errors
}
}
// 401 - 未认证
class UnauthorizedError extends AppError {
constructor(message = "未授权访问") {
super(message, 401, "UNAUTHORIZED")
}
}
// 403 - 禁止访问
class ForbiddenError extends AppError {
constructor(message = "禁止访问") {
super(message, 403, "FORBIDDEN")
}
}
// 404 - 资源未找到
class NotFoundError extends AppError {
constructor(message = "资源未找到", resource = null) {
super(message, 404, "NOT_FOUND")
this.resource = resource
}
}
// 409 - 冲突
class ConflictError extends AppError {
constructor(message = "资源冲突") {
super(message, 409, "CONFLICT")
}
}
// 422 - 无法处理
class UnprocessableEntityError extends AppError {
constructor(message = "无法处理的实体") {
super(message, 422, "UNPROCESSABLE_ENTITY")
}
}
// 429 - 请求过多
class TooManyRequestsError extends AppError {
constructor(message = "请求过于频繁") {
super(message, 429, "TOO_MANY_REQUESTS")
}
}
// 500 - 服务器错误
class InternalServerError extends AppError {
constructor(message = "服务器内部错误") {
super(message, 500, "INTERNAL_SERVER_ERROR")
}
}
// 503 - 服务不可用
class ServiceUnavailableError extends AppError {
constructor(message = "服务暂时不可用") {
super(message, 503, "SERVICE_UNAVAILABLE")
}
}
module.exports = {
AppError,
BadRequestError,
ValidationError,
UnauthorizedError,
ForbiddenError,
NotFoundError,
ConflictError,
UnprocessableEntityError,
TooManyRequestsError,
InternalServerError,
ServiceUnavailableError
}使用自定义错误
javascript
const {
NotFoundError,
ValidationError,
UnauthorizedError,
ConflictError
} = require("./errors")
const asyncHandler = require("./middleware/asyncHandler")
// 用户不存在
app.get("/users/:id", asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id)
if (!user) {
throw new NotFoundError("用户不存在", "User")
}
res.json(user)
}))
// 验证错误
app.post("/users", asyncHandler(async (req, res) => {
const { username, email, password } = req.body
const errors = []
if (!username) errors.push({ field: "username", message: "用户名必填" })
if (!email) errors.push({ field: "email", message: "邮箱必填" })
if (!password) errors.push({ field: "password", message: "密码必填" })
if (errors.length > 0) {
throw new ValidationError("数据验证失败", errors)
}
const user = await User.create({ username, email, password })
res.status(201).json(user)
}))
// 未认证
app.get("/profile", asyncHandler(async (req, res) => {
if (!req.user) {
throw new UnauthorizedError("请先登录")
}
res.json(req.user)
}))
// 资源冲突
app.post("/users", asyncHandler(async (req, res) => {
const existingUser = await User.findOne({ email: req.body.email })
if (existingUser) {
throw new ConflictError("邮箱已被注册")
}
// ...
}))统一错误处理方案
目录结构
code
project/
├── app.js
├── middleware/
│ ├── asyncHandler.js # 异步包装器
│ └── errorHandler.js # 错误处理中间件
├── errors/
│ ├── AppError.js # 基础错误类
│ └── index.js # 错误类导出
└── controllers/
└── userController.js异步包装器
javascript
// middleware/asyncHandler.js
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next)
}
module.exports = asyncHandler错误处理中间件
javascript
// middleware/errorHandler.js
const { AppError } = require("../errors")
const errorHandler = (err, req, res, next) => {
// 记录请求信息
const requestInfo = {
method: req.method,
url: req.originalUrl,
ip: req.ip,
userId: req.user?.id
}
// 处理不同类型的错误
let status = 500
let message = "服务器内部错误"
let code = "INTERNAL_SERVER_ERROR"
let details = null
// 自定义错误
if (err instanceof AppError) {
status = err.status
message = err.message
code = err.code
details = err.errors || null
}
// Mongoose 验证错误
else if (err.name === "ValidationError") {
status = 400
message = "数据验证失败"
code = "VALIDATION_ERROR"
details = Object.values(err.errors).map(e => ({
field: e.path,
message: e.message
}))
}
// Mongoose CastError(无效 ID)
else if (err.name === "CastError") {
status = 400
message = `无效的 ${err.path}: ${err.value}`
code = "INVALID_ID"
}
// Mongoose 重复键错误
else if (err.code === 11000) {
status = 409
const field = Object.keys(err.keyValue)[0]
message = `${field} 已存在`
code = "DUPLICATE_KEY"
}
// JWT 错误
else if (err.name === "JsonWebTokenError") {
status = 401
message = "无效的令牌"
code = "INVALID_TOKEN"
}
else if (err.name === "TokenExpiredError") {
status = 401
message = "令牌已过期"
code = "TOKEN_EXPIRED"
}
// Multer 文件上传错误
else if (err.code === "LIMIT_FILE_SIZE") {
status = 400
message = "文件大小超出限制"
code = "FILE_TOO_LARGE"
}
else if (err.code === "LIMIT_UNEXPECTED_FILE") {
status = 400
message = "意外的文件字段"
code = "UNEXPECTED_FILE_FIELD"
}
// 未知错误
else {
console.error("未处理的错误:", err)
}
// 构建响应
const response = {
success: false,
error: {
message,
code,
...(details && { details })
}
}
// 开发环境返回堆栈
if (process.env.NODE_ENV === "development") {
response.error.stack = err.stack
response.error.request = requestInfo
}
// 发送响应
res.status(status).json(response)
}
module.exports = errorHandler应用入口配置
javascript
// app.js
const express = require("express")
const app = express()
const asyncHandler = require("./middleware/asyncHandler")
const errorHandler = require("./middleware/errorHandler")
const { NotFoundError } = require("./errors")
// 中间件
app.use(express.json())
// 路由
app.use("/api/users", require("./routes/users"))
app.use("/api/products", require("./routes/products"))
// 健康检查(不受错误处理影响)
app.get("/health", (req, res) => {
res.json({ status: "ok", timestamp: new Date().toISOString() })
})
// 404 处理
app.use((req, res, next) => {
next(new NotFoundError(`路由 ${req.originalUrl} 不存在`))
})
// 错误处理中间件
app.use(errorHandler)
module.exports = app常见错误场景处理
数据库错误
javascript
const { ValidationError, ConflictError } = require("../errors")
const asyncHandler = require("../middleware/asyncHandler")
// MongoDB 错误处理
exports.createUser = asyncHandler(async (req, res) => {
try {
const user = await User.create(req.body)
res.status(201).json(user)
} catch (error) {
// 验证错误
if (error.name === "ValidationError") {
const messages = Object.values(error.errors).map(e => e.message)
throw new ValidationError(messages.join(", "))
}
// 重复键错误
if (error.code === 11000) {
const field = Object.keys(error.keyValue)[0]
throw new ConflictError(`${field} 已存在`)
}
// 其他错误继续抛出
throw error
}
})
// Sequelize 错误处理
exports.createProduct = asyncHandler(async (req, res) => {
try {
const product = await Product.create(req.body)
res.status(201).json(product)
} catch (error) {
if (error.name === "SequelizeValidationError") {
const messages = error.errors.map(e => e.message)
throw new ValidationError(messages.join(", "))
}
if (error.name === "SequelizeUniqueConstraintError") {
throw new ConflictError("资源已存在")
}
throw error
}
})JWT 认证错误
javascript
const jwt = require("jsonwebtoken")
const { UnauthorizedError } = require("../errors")
const authMiddleware = (req, res, next) => {
try {
// 获取 token
const authHeader = req.headers.authorization
if (!authHeader?.startsWith("Bearer ")) {
throw new UnauthorizedError("请提供认证令牌")
}
const token = authHeader.split(" ")[1]
// 验证 token
const decoded = jwt.verify(token, process.env.JWT_SECRET)
req.user = decoded
next()
} catch (error) {
if (error.name === "JsonWebTokenError") {
next(new UnauthorizedError("无效的令牌"))
} else if (error.name === "TokenExpiredError") {
next(new UnauthorizedError("令牌已过期,请重新登录"))
} else if (error.name === "NotBeforeError") {
next(new UnauthorizedError("令牌尚未生效"))
} else {
next(error)
}
}
}文件上传错误
javascript
const multer = require("multer")
const { BadRequestError } = require("../errors")
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
files: 5
},
fileFilter: (req, file, cb) => {
const allowedTypes = ["image/jpeg", "image/png", "image/webp"]
if (!allowedTypes.includes(file.mimetype)) {
return cb(new BadRequestError("只支持 JPG、PNG、WEBP 格式图片"), false)
}
cb(null, true)
}
})
// 错误处理包装器
const uploadMiddleware = (field) => (req, res, next) => {
upload.single(field)(req, res, (err) => {
if (err instanceof multer.MulterError) {
// Multer 错误
switch (err.code) {
case "LIMIT_FILE_SIZE":
return next(new BadRequestError("文件大小不能超过 5MB"))
case "LIMIT_FILE_COUNT":
return next(new BadRequestError("最多上传 5 个文件"))
case "LIMIT_UNEXPECTED_FILE":
return next(new BadRequestError(`意外的文件字段: ${err.field}`))
default:
return next(new BadRequestError(err.message))
}
} else if (err) {
// 其他错误
return next(err)
}
next()
})
}
// 使用
app.post("/upload", uploadMiddleware("avatar"), (req, res) => {
res.json({ message: "上传成功", file: req.file })
})第三方 API 错误
javascript
const axios = require("axios")
const { ServiceUnavailableError, BadRequestError } = require("../errors")
const asyncHandler = require("../middleware/asyncHandler")
exports.callExternalAPI = asyncHandler(async (req, res) => {
try {
const response = await axios.get("https://api.example.com/data", {
timeout: 5000
})
res.json(response.data)
} catch (error) {
if (error.code === "ECONNABORTED") {
throw new ServiceUnavailableError("外部服务响应超时")
}
if (error.response) {
// 服务端返回错误
const { status, data } = error.response
if (status === 400) {
throw new BadRequestError(data.message || "请求参数错误")
}
if (status === 401) {
throw new UnauthorizedError("外部服务认证失败")
}
if (status >= 500) {
throw new ServiceUnavailableError("外部服务暂时不可用")
}
}
if (error.code === "ENOTFOUND") {
throw new ServiceUnavailableError("无法连接到外部服务")
}
throw error
}
})全局错误处理
未捕获异常处理
javascript
// 未捕获的异常
process.on("uncaughtException", (error) => {
console.error("未捕获的异常:", error)
// 记录日志后退出进程
// PM2 会自动重启
process.exit(1)
})
// 未处理的 Promise 拒绝
process.on("unhandledRejection", (reason, promise) => {
console.error("未处理的 Promise 拒绝:", reason)
// 可以选择退出进程
// process.exit(1)
})优雅关闭
javascript
const server = app.listen(3000)
// 优雅关闭
const gracefulShutdown = () => {
console.log("正在关闭服务器...")
server.close(async () => {
console.log("HTTP 服务器已关闭")
// 关闭数据库连接
await mongoose.connection.close()
console.log("数据库连接已关闭")
process.exit(0)
})
// 强制关闭超时
setTimeout(() => {
console.error("强制关闭")
process.exit(1)
}, 10000)
}
process.on("SIGTERM", gracefulShutdown)
process.on("SIGINT", gracefulShutdown)错误日志记录
Winston 日志配置
javascript
// config/logger.js
const winston = require("winston")
const path = require("path")
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || "info",
format: winston.format.combine(
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: "express-app" },
transports: [
// 错误日志
new winston.transports.File({
filename: path.join(__dirname, "../logs/error.log"),
level: "error",
maxsize: 5242880, // 5MB
maxFiles: 5
}),
// 所有日志
new winston.transports.File({
filename: path.join(__dirname, "../logs/combined.log"),
maxsize: 5242880,
maxFiles: 5
})
]
})
// 开发环境控制台输出
if (process.env.NODE_ENV !== "production") {
logger.add(
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
})
)
}
module.exports = logger在错误处理中使用
javascript
const logger = require("../config/logger")
const errorHandler = (err, req, res, next) => {
const errorInfo = {
message: err.message,
status: err.status || 500,
method: req.method,
url: req.originalUrl,
ip: req.ip,
userAgent: req.get("User-Agent"),
userId: req.user?.id,
body: req.body,
params: req.params,
query: req.query,
stack: err.stack
}
// 记录错误日志
logger.error("Request Error", errorInfo)
// 返回响应
res.status(errorInfo.status).json({
success: false,
error: {
message: err.message,
status: errorInfo.status
}
})
}Sentry 错误监控
javascript
const Sentry = require("@sentry/node")
// 初始化
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 1.0
})
// 在所有中间件之前
app.use(Sentry.Handlers.requestHandler())
app.use(Sentry.Handlers.tracingHandler())
// ... 路由 ...
// Sentry 错误处理(在自定义错误处理之前)
app.use(Sentry.Handlers.errorHandler({
shouldHandleError(error) {
// 只捕获 4xx 和 5xx 错误
return error.status >= 400 && error.status < 600
}
}))
// 自定义错误处理
app.use((err, req, res, next) => {
res.status(err.status || 500).json({
success: false,
error: { message: err.message }
})
})最佳实践
错误响应格式统一
javascript
// 成功响应
{
"success": true,
"data": { ... }
}
// 错误响应
{
"success": false,
"error": {
"message": "错误描述",
"code": "ERROR_CODE",
"status": 400,
"details": [...] // 可选
}
}HTTP 状态码规范
| 状态码 | 含义 | 使用场景 |
|---|---|---|
200 | 成功 | GET、PUT、PATCH 成功 |
201 | 已创建 | POST 创建资源成功 |
204 | 无内容 | DELETE 成功 |
400 | 请求错误 | 参数缺失、格式错误 |
401 | 未认证 | 缺少或无效的认证 |
403 | 禁止访问 | 无权限访问 |
404 | 未找到 | 资源不存在 |
409 | 冲突 | 资源已存在 |
422 | 无法处理 | 语义错误 |
429 | 请求过多 | 触发限流 |
500 | 服务器错误 | 未知错误 |
503 | 服务不可用 | 维护中 |
环境区分
javascript
const errorHandler = (err, req, res, next) => {
const isDev = process.env.NODE_ENV === "development"
const isProd = process.env.NODE_ENV === "production"
res.status(err.status || 500).json({
success: false,
error: {
message: isProd && err.isOperational === false
? "服务器内部错误"
: err.message,
status: err.status || 500,
// 开发环境返回详细信息
...(isDev && {
stack: err.stack,
details: err
})
}
})
}安全考虑
javascript
// ❌ 泄露敏感信息
throw new Error(`用户 ${email} 不存在,请检查邮箱地址`)
// ✅ 安全的错误消息
throw new NotFoundError("用户不存在")
// ❌ 返回完整错误堆栈给用户
res.status(500).json({ error: err.stack })
// ✅ 仅在开发环境返回堆栈
res.status(500).json({
error: {
message: err.message,
...(process.env.NODE_ENV === "development" && { stack: err.stack })
}
})常见问题
Q1: 错误处理中间件为什么没有执行?
javascript
// ❌ 错误:放在路由之前
app.use(errorHandler)
app.get("/users", (req, res) => { ... })
// ✅ 正确:放在所有路由之后
app.get("/users", (req, res) => { ... })
app.use(errorHandler)Q2: 异步路由错误如何处理?
javascript
// ❌ Express 4.x 不会捕获
app.get("/users", async (req, res) => {
const users = await User.find() // 错误不会传递
res.json(users)
})
// ✅ 方案一:try-catch
app.get("/users", async (req, res, next) => {
try {
const users = await User.find()
res.json(users)
} catch (error) {
next(error)
}
})
// ✅ 方案二:包装器
app.get("/users", asyncHandler(async (req, res) => {
const users = await User.find()
res.json(users)
}))Q3: 如何处理 404 错误?
javascript
// 放在所有路由之后、错误处理之前
app.use((req, res, next) => {
const error = new Error(`未找到: ${req.originalUrl}`)
error.status = 404
next(error)
})
app.use(errorHandler)Q4: 如何区分操作错误和程序错误?
javascript
// 操作错误:可预期的,应该处理
class AppError extends Error {
constructor(message, status) {
super(message)
this.status = status
this.isOperational = true // 标记为操作错误
}
}
// 错误处理中间件中区分
app.use((err, req, res, next) => {
if (err.isOperational) {
// 操作错误:返回友好的错误消息
res.status(err.status).json({ error: err.message })
} else {
// 程序错误:记录日志,返回通用错误
logger.error(err)
res.status(500).json({ error: "服务器内部错误" })
}
})Q5: 如何处理 JSON 解析错误?
javascript
// 专门的 JSON 解析错误处理
app.use((err, req, res, next) => {
if (err instanceof SyntaxError && err.status === 400 && "body" in err) {
return res.status(400).json({
success: false,
error: {
message: "JSON 格式错误",
code: "INVALID_JSON"
}
})
}
next(err)
})