{T}

Express 错误处理

错误处理是 Express 应用的关键环节。完善的错误处理机制可以确保应用的稳定性、提供良好的用户体验,并方便开发调试。


错误处理架构

错误传播流程

code
路由处理函数
      │
      │ throw Error 或 next(error)
      ▼
┌─────────────────────────────────────────────┐
│              Express 错误传播                │
├─────────────────────────────────────────────┤
│  1. 同步错误自动捕获                         │
│  2. 异步错误需 next(err) 传递                │
│  3. 按顺序查找错误处理中间件                  │
└─────────────────────────────────────────────┘
      │
      ▼
┌─────────────────────────────────────────────┐
│          错误处理中间件                       │
│          (err, req, res, next)              │
├─────────────────────────────────────────────┤
│  • 记录日志                                   │
│  • 标准化响应格式                             │
│  • 区分环境返回详情                           │
└─────────────────────────────────────────────┘
      │
      ▼
   客户端响应

错误类型分类

类型来源捕获方式示例
同步错误同步代码 throwExpress 自动捕获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)
})

参考资源