异常处理
异常处理是 Node.js 应用程序开发中的核心环节。合理的异常处理策略能够:
- 提高应用的稳定性和可靠性
- 快速定位和修复问题
- 提升用户体验
- 便于生产环境的问题追踪
Node.js 提供多种异常处理机制:
- 同步异常:
try...catch...finally - 异步异常:回调函数、Promise、async/await
- 全局异常:
uncaughtException、unhandledRejection - 自定义异常:继承 Error 类创建业务错误
使用 console 基础调试
console 模块是 Node.js 内置的最简单、最直接的调试工具。通过在代码中插入日志输出,可以实时观察程序状态、变量值和执行流程
日志级别
console 对象提供多种方法,用于输出不同级别的日志信息,以便更好地区分和过滤
console.log(): 最常用的输出方法,用于打印通用信息console.info(): 与log类似,用于输出提示性信息console.warn(): 输出警告信息,通常表示潜在问题console.error(): 输出错误信息,表示已发生错误console.debug(): 用于输出调试信息,比log更详尽
console.log("这是一条普通日志")
console.info("这是一条提示信息")
console.warn("警告:某个函数即将被废弃")
console.error(new Error("发生了一个严重错误"))格式化输出
console.log 支持 C 语言 printf 风格的占位符,可以更清晰地格式化输出
%s: 字符串%d或%i: 整数%f: 浮点数%o: 可交互的对象%O: 更详细的对象信息%%: 百分号
const user = { name: "Alice", age: 30 }
console.log("用户信息: %o", user)
// 输出: 用户信息: { name: 'Alice', age: 30 }
for (let i = 1; i <= 3; i++) {
console.log("第 %d 次循环", i)
}
// 输出:
// 第 1 次循环
// 第 2 次循环
// 第 3 次循环计时器
console.time() 和 console.timeEnd() 可以用来测量代码块的执行时间
console.time("my-timer")
// ... 一段耗时操作 ...
for (let i = 0; i < 100000; i++) {}
console.timeEnd("my-timer")
// 输出: my-timer: 0.81ms (时间因机器而异)最佳实践: 虽然
console很方便,但在生产环境中应避免过多使用,因为它会产生大量 I/O 操作,影响性能。推荐使用专门的日志库(如winston或pino)来管理日志
异常处理机制
异常处理是保证程序健壮性的重要一环。Node.js 中的异常分为同步异常和异步异常
同步异常
同步代码中的异常可以通过 try...catch 语句块来捕获
Error 对象
当错误发生时,Node.js 通常会抛出 Error 对象。Error 对象包含三个核心属性:
name: 错误类型 (如TypeError,ReferenceError)。message: 错误的描述信息。stack: 错误的堆栈跟踪,显示了错误发生的位置和调用路径。
try {
// 一段可能出错的代码
const data = JSON.parse('{ "name": "Alice", }') // 错误的 JSON 格式
} catch (error) {
console.error("捕获到错误!")
console.error("错误名称:", error.name) // SyntaxError
console.error("错误信息:", error.message) // Unexpected token } in JSON at position 24
console.error("堆栈跟踪:", error.stack)
}throw 语句
可以使用 throw 关键字主动抛出异常。最佳实践是抛出 Error 对象或其子类的实例
function divide(a, b) {
if (b === 0) {
throw new Error("除数不能为零")
}
return a / b
}
try {
console.log(divide(10, 0))
} catch (error) {
console.error(error.message) // 输出: 除数不能为零
}try...catch...finally
finally 代码块中的代码无论是否发生异常,都总是会被执行。这对于释放资源(如关闭文件句柄、数据库连接)非常有用
const fs = require("fs")
let fileHandle
try {
fileHandle = fs.openSync("test.txt", "r")
// ... 对文件进行操作 ...
} catch (error) {
console.error("文件操作失败:", error.message)
} finally {
if (fileHandle) {
fs.closeSync(fileHandle)
console.log("文件已关闭")
}
}异步异常
异步代码中的异常无法被外部的 try...catch 直接捕获,因为异步回调函数在另一个执行上下文中运行
回调函数 (Error-First Pattern)
在 Node.js 的传统回调风格中,回调函数的第一个参数通常是 error 对象。如果操作成功,error 为 null;如果失败,error 则包含错误信息
const fs = require("fs")
fs.readFile("non-existent-file.txt", "utf8", (err, data) => {
if (err) {
console.error("读取文件失败:", err)
return
}
console.log("文件内容:", data)
})Promise
Promise 通过 .catch() 方法或 try...catch 与 async/await 结合来处理异步异常
使用 .catch():
const fs = require("fs").promises
fs.readFile("non-existent-file.txt", "utf8")
.then((data) => console.log(data))
.catch((err) => console.error("Promise 捕获到错误:", err))使用 async/await:
async/await 让异步代码看起来像同步代码,并且可以直接使用 try...catch 来捕获 await 表达式中的异常
const fs = require("fs").promises
async function readFileAsync() {
try {
const data = await fs.readFile("non-existent-file.txt", "utf8")
console.log(data)
} catch (err) {
console.error("async/await 捕获到错误:", err)
}
}
readFileAsync()全局未捕获异常
如果一个异常(无论是同步还是异步)没有被任何地方捕获,它会成为一个“未捕获异常”,并可能导致 Node.js 进程崩溃。可以通过监听 process 对象的事件来处理这类异常,作为最后的防线
-
process.on('uncaughtException', (err, origin) => { ... }):- 捕获所有未被
try...catch捕获的同步异常 - 警告: 在这里执行异步操作是不安全的。官方建议的用途是记录错误,然后优雅地关闭进程
- 捕获所有未被
-
process.on('unhandledRejection', (reason, promise) => { ... }):- 捕获所有没有
.catch()处理的 Promise 拒绝。
- 捕获所有没有
process.on("uncaughtException", (err) => {
console.error("有一个未捕获的同步异常:", err)
// 在这里记录日志,然后退出进程
process.exit(1)
})
process.on("unhandledRejection", (reason, promise) => {
console.error("有一个未处理的 Promise 拒绝:", reason)
// 同样,记录日志并考虑是否需要退出
})
// 触发 uncaughtException
throw new Error("这是一个同步错误")
// 触发 unhandledRejection
Promise.reject(new Error("这是一个 Promise 拒绝"))重要:
uncaughtException是一个粗暴的异常处理机制。依赖它而不是正确地处理错误是一个坏习惯。它的主要目的是在进程崩溃前进行最后的清理和记录
常见错误类型与处理策略
理解 Node.js 中的常见错误类型有助于快速定位问题
| Error 子类 | 说明 | 常见场景与处理策略 |
|---|---|---|
ReferenceError | 试图访问一个未定义的变量。 | 场景: 拼写错误、变量作用域问题。<br>策略: 仔细检查变量名和作用域,使用 linter 工具可以有效预防。 |
TypeError | 值的类型不符合预期。 | 场景: 对 null 或 undefined 调用方法、函数参数类型错误。<br>策略: 在使用变量前进行类型检查和空值检查。 |
RangeError | 数值超出允许的范围。 | 场景: 无效的数组长度、递归调用没有出口导致栈溢出。<br>策略: 检查边界条件,确保递归有正确的终止条件。 |
SyntaxError | 代码不符合 JavaScript 语法。 | 场景: 括号不匹配、关键字错误。<br>策略: 这种错误在代码解析阶段就会抛出,通常由开发环境或构建工具直接报告。 |
SystemError | 操作系统级别的错误。 | 场景: 文件不存在 (ENOENT)、权限不足 (EACCES)、端口被占用 (EADDRINUSE)。<br>策略: 根据错误码 (code) 进行相应的处理,例如提示用户检查文件路径或更换端口。 |
示例:处理 SystemError
const http = require("http")
const server = http.createServer((req, res) => {
res.end("Hello World")
})
server.on("error", (err) => {
if (err.code === "EADDRINUSE") {
console.error("端口 3000 已被占用,请尝试其他端口。")
} else {
console.error("服务器发生未知错误:", err)
}
})
server.listen(3000, () => {
console.log("服务器运行在 http://localhost:3000")
})高级调试工具
除了基础的 console 调试,Node.js 提供了更强大的调试工具。
Node.js 内置调试器
Node.js 内置了调试器,可以通过 inspect 标志启动:
node inspect app.js
# 或使用现代调试协议
node --inspect app.js
node --inspect-brk app.js # 在第一行断点然后在 Chrome 浏览器中访问 chrome://inspect 进行可视化调试。
VS Code 调试配置
在项目根目录创建 .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "启动程序",
"program": "${workspaceFolder}/app.js",
"console": "integratedTerminal"
},
{
"type": "node",
"request": "attach",
"name": "附加到进程",
"port": 9229
}
]
}常用调试技巧
1. 使用 debugger 语句
function complexCalculation(n) {
debugger // 代码会在此处暂停
let result = 0
for (let i = 0; i < n; i++) {
result += i
}
return result
}2. 使用 util.inspect 深度检查对象
const util = require('util')
const obj = {
level1: {
level2: {
level3: {
value: 'deep'
}
}
}
}
console.log(util.inspect(obj, {
depth: null, // 无限深度
colors: true, // 彩色输出
compact: false // 格式化输出
}))3. 性能分析
# 生成 CPU 分析报告
node --prof app.js
# 处理分析报告
node --prof-process isolate-*.log > processed.txt
# 使用 Chrome DevTools 进行性能分析
node --inspect app.js
# 然后在 Chrome DevTools 的 Performance 标签中录制第三方调试工具
| 工具 | 说明 | 适用场景 |
|---|---|---|
ndb | Node.js 调试工具 | 更好的调试体验 |
node-inspector | 基于 Chrome 的调试器 | 可视化调试 |
ironNode | Node.js 调试 IDE | 复杂应用调试 |
自定义错误类型
创建自定义错误类
在实际项目中,创建自定义错误类型可以更好地区分和处理不同类型的错误:
// 基础自定义错误类
class AppError extends Error {
constructor(message, statusCode = 500) {
super(message)
this.name = this.constructor.name
this.statusCode = statusCode
this.isOperational = true // 标识可预期的错误
Error.captureStackTrace(this, this.constructor)
}
}
// 数据库错误
class DatabaseError extends AppError {
constructor(message = '数据库操作失败') {
super(message, 500)
this.errorType = 'DATABASE_ERROR'
}
}
// 验证错误
class ValidationError extends AppError {
constructor(message = '数据验证失败', errors = []) {
super(message, 400)
this.errorType = 'VALIDATION_ERROR'
this.errors = errors // 详细错误信息
}
}
// 认证错误
class AuthenticationError extends AppError {
constructor(message = '认证失败') {
super(message, 401)
this.errorType = 'AUTHENTICATION_ERROR'
}
}
// 授权错误
class AuthorizationError extends AppError {
constructor(message = '权限不足') {
super(message, 403)
this.errorType = 'AUTHORIZATION_ERROR'
}
}
// 资源未找到错误
class NotFoundError extends AppError {
constructor(resource = '资源') {
super(`${resource}不存在`, 404)
this.errorType = 'NOT_FOUND_ERROR'
}
}使用自定义错误
// 在业务代码中使用
async function getUserById(id) {
if (!id || isNaN(id)) {
throw new ValidationError('用户 ID 格式不正确', [
{ field: 'id', message: 'ID 必须是数字' }
])
}
const user = await db.users.findById(id)
if (!user) {
throw new NotFoundError('用户')
}
return user
}
// 在 Express 中统一处理
app.use((err, req, res, next) => {
// 操作性错误:可以安全地向客户端返回错误信息
if (err.isOperational) {
return res.status(err.statusCode).json({
success: false,
error: {
type: err.errorType,
message: err.message,
errors: err.errors // 验证错误详细信息
}
})
}
// 编程错误或未知错误:不泄露详细信息
logger.error('未预期的错误:', err)
res.status(500).json({
success: false,
error: {
type: 'INTERNAL_ERROR',
message: '服务器内部错误'
}
})
})错误类型设计最佳实践
// 1. 带错误码的自定义错误
class ErrorCode {
static USER_NOT_FOUND = 'USER_001'
static INVALID_PASSWORD = 'USER_002'
static USER_ALREADY_EXISTS = 'USER_003'
}
class UserError extends AppError {
constructor(message, errorCode, statusCode = 400) {
super(message, statusCode)
this.code = errorCode
}
}
// 使用
throw new UserError('用户不存在', ErrorCode.USER_NOT_FOUND, 404)
// 2. 带元数据的错误
class NetworkError extends Error {
constructor(message, { url, method, statusCode, responseBody }) {
super(message)
this.name = 'NetworkError'
this.url = url
this.method = method
this.statusCode = statusCode
this.responseBody = responseBody
}
}
// 使用
throw new NetworkError('请求失败', {
url: 'https://api.example.com/users',
method: 'GET',
statusCode: 503,
responseBody: '{"error": "Service Unavailable"}'
})错误传播与链式处理
错误链(Error Cause)
从 Node.js v16.9.0 开始,支持错误链,可以在一个错误中包含导致它的原始错误:
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`)
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
return await response.json()
} catch (error) {
// 创建新错误,并保留原始错误信息
throw new AppError('获取用户数据失败', {
cause: error // 原始错误
})
}
}
// 捕获后可以追溯错误链
try {
await fetchUserData(123)
} catch (error) {
console.error('顶层错误:', error.message)
if (error.cause) {
console.error('原始错误:', error.cause.message)
console.error('原始堆栈:', error.cause.stack)
}
}错误包装模式
将底层错误包装为更高级的业务错误:
class UserRepository {
async findById(id) {
try {
const connection = await db.getConnection()
const [rows] = await connection.execute(
'SELECT * FROM users WHERE id = ?',
[id]
)
return rows[0]
} catch (error) {
// 包装数据库错误
if (error.code === 'ECONNREFUSED') {
throw new DatabaseError('数据库连接失败')
}
if (error.code === 'ER_ACCESS_DENIED_ERROR') {
throw new DatabaseError('数据库访问权限不足')
}
// 其他数据库错误
throw new DatabaseError(`数据库查询失败: ${error.message}`)
}
}
}异步错误边界
创建错误边界函数来统一处理异步错误:
// 高阶函数:包装异步路由处理器
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next))
.catch(next)
}
}
// 使用
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await getUserById(req.params.id)
res.json(user)
}))
// 或使用 express-async-errors 库(推荐)
// 无需手动包装,直接使用 async/await
require('express-async-errors')
app.get('/users/:id', async (req, res) => {
const user = await getUserById(req.params.id)
res.json(user)
})错误处理策略
重试机制
对于临时性错误(网络抖动、服务暂时不可用),实现自动重试:
async function retry(fn, options = {}) {
const {
maxAttempts = 3,
delay = 1000,
backoff = 'exponential', // 'linear' 或 'exponential'
shouldRetry = (error) => true // 判断是否应该重试的函数
} = options
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn()
} catch (error) {
if (attempt === maxAttempts || !shouldRetry(error)) {
throw error
}
const waitTime = backoff === 'exponential'
? delay * Math.pow(2, attempt - 1)
: delay * attempt
console.log(`第 ${attempt} 次尝试失败,${waitTime}ms 后重试...`)
await new Promise(resolve => setTimeout(resolve, waitTime))
}
}
}
// 使用
try {
const data = await retry(
() => fetchExternalAPI('https://api.example.com/data'),
{
maxAttempts: 3,
delay: 1000,
backoff: 'exponential',
shouldRetry: (error) => {
// 只重试网络错误和 5xx 错误
return error.code === 'ECONNREFUSED' ||
(error.statusCode >= 500 && error.statusCode < 600)
}
}
)
} catch (error) {
console.error('所有重试都失败:', error)
}断路器模式(Circuit Breaker)
防止故障传播,避免重复请求已知失败的服务:
class CircuitBreaker {
constructor(fn, options = {}) {
this.fn = fn
this.failureThreshold = options.failureThreshold || 5
this.resetTimeout = options.resetTimeout || 60000 // 1分钟
this.state = 'CLOSED' // CLOSED, OPEN, HALF_OPEN
this.failures = 0
this.nextAttempt = Date.now()
}
async call(...args) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('断路器处于打开状态,请求被拒绝')
}
this.state = 'HALF_OPEN'
}
try {
const result = await this.fn(...args)
this.onSuccess()
return result
} catch (error) {
this.onFailure()
throw error
}
}
onSuccess() {
this.failures = 0
this.state = 'CLOSED'
}
onFailure() {
this.failures++
if (this.failures >= this.failureThreshold) {
this.state = 'OPEN'
this.nextAttempt = Date.now() + this.resetTimeout
}
}
getState() {
return {
state: this.state,
failures: this.failures,
nextAttempt: new Date(this.nextAttempt)
}
}
}
// 使用
const breaker = new CircuitBreaker(
() => fetchExternalAPI('https://api.example.com/data'),
{ failureThreshold: 3, resetTimeout: 30000 }
)
try {
const data = await breaker.call()
} catch (error) {
if (error.message.includes('断路器')) {
// 返回降级数据或缓存
return getCachedData()
}
throw error
}超时处理
为异步操作设置超时,防止无限等待:
function timeout(promise, ms) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`操作超时 (${ms}ms)`)), ms)
)
])
}
// 使用
try {
const data = await timeout(
fetchDataFromDatabase(),
5000 // 5秒超时
)
} catch (error) {
if (error.message.includes('超时')) {
console.error('数据库查询超时')
}
}
// 或使用 AbortController (Node.js v15+)
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
try {
const response = await fetch(url, {
...options,
signal: controller.signal
})
clearTimeout(timeoutId)
return response
} catch (error) {
clearTimeout(timeoutId)
if (error.name === 'AbortError') {
throw new Error(`请求超时 (${timeout}ms)`)
}
throw error
}
}错误日志与监控
结构化日志记录
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: 'user-service' },
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()
}))
}
// 错误日志记录
try {
await riskyOperation()
} catch (error) {
logger.error('操作失败', {
error: {
message: error.message,
stack: error.stack,
code: error.code
},
context: {
userId: '123',
operation: 'riskyOperation'
}
})
throw error
}错误追踪集成
集成错误监控服务(如 Sentry):
const Sentry = require('@sentry/node')
// 初始化 Sentry
Sentry.init({
dsn: 'https://example@sentry.io/123',
environment: process.env.NODE_ENV,
// 设置错误采样率
tracesSampleRate: 1.0
})
// 捕获异常
try {
riskyOperation()
} catch (error) {
Sentry.captureException(error)
throw error
}
// 添加上下文信息
Sentry.setUser({ id: '123', email: 'user@example.com' })
Sentry.setTag('page_route', '/users/profile')
Sentry.setContext('character', {
name: 'Mighty Fighter',
level: 19
})
// Express 集成
app.use(Sentry.Handlers.requestHandler())
// ... 路由 ...
app.use(Sentry.Handlers.errorHandler())调试与异常处理的最佳实践
开发阶段
- 使用 Linter 和 Formatter: 工具如 ESLint 和 Prettier 可以在编码阶段就发现大量潜在的语法和风格问题
- 编写单元测试: 测试是检验代码逻辑、模拟边界情况和捕获回归错误的最佳方式
- 使用 TypeScript: 类型系统可以在编译时捕获大量错误
- 启用严格模式: 在文件开头添加
'use strict'或使用--strict标志
// 启用严格模式
'use strict'
// 或在 package.json 中配置
{
"type": "module",
"engines": {
"node": ">=18.0.0"
}
}生产环境
- 日志分级: 在生产环境中使用专门的日志库,并根据重要性设置不同的日志级别(如
info,warn,error),以便于监控和报警 - 优雅地处理错误: 不要简单地吞掉错误(
catch (e) {})。至少要记录下来。对于可恢复的错误(如网络抖动),可以尝试重试机制;对于不可恢复的错误,应记录详细信息并让应用失败(fail-fast) - 避免
uncaughtException作为主要错误处理: 它应该是最后的防线,而不是常规的错误处理逻辑 - 为异步操作添加超时: 对于网络请求或数据库查询等 I/O 操作,设置合理的超时时间,防止无限等待
错误处理清单
// ✅ 正确的错误处理示例
async function goodErrorHandling(userId) {
try {
// 参数验证
if (!userId) {
throw new ValidationError('用户 ID 不能为空')
}
// 设置超时
const user = await timeout(
User.findById(userId),
5000
)
if (!user) {
throw new NotFoundError('用户')
}
return user
} catch (error) {
// 记录错误
logger.error('获取用户失败', {
userId,
error: error.message,
stack: error.stack
})
// 重新抛出业务错误
if (error instanceof AppError) {
throw error
}
// 包装未知错误
throw new AppError('获取用户失败', {
cause: error
})
}
}
// ❌ 错误的错误处理示例
async function badErrorHandling(userId) {
try {
const user = await User.findById(userId)
return user
} catch (error) {
// 吞掉错误,不做任何处理
console.log(error)
return null
}
}
## 综合案例:健壮的 Web 服务器
下面是一个简单的 Express 服务器示例,它结合了日志、异步错误处理和全局错误处理中间件:
- 使用 `winston` 进行结构化日志记录
- 使用 `async/await` 和 `try/catch` 处理异步路由的错误
- 创建一个集中的错误处理中间件来标准化错误响应
- 监听全局事件作为最后的保障
```javascript
const express = require("express")
const winston = require("winston") // 一个流行的日志库
// 1. 配置日志记录器
const logger = winston.createLogger({
level: "info",
format: winston.format.json(),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: "error.log", level: "error" }),
new winston.transports.File({ filename: "combined.log" })
]
})
const app = express()
// 2. 中间件:记录每个请求
app.use((req, res, next) => {
logger.info(`${req.method} ${req.url}`)
next()
})
// 3. 异步路由,并正确处理 Promise 拒绝
app.get("/data", async (req, res, next) => {
try {
// 模拟一个异步操作,可能会失败
const data = await Promise.reject(new Error("无法从数据库获取数据"))
res.json(data)
} catch (error) {
// 将错误传递给 Express 的错误处理中间件
next(error)
}
})
app.get("/user/:id", (req, res, next) => {
const { id } = req.params
if (isNaN(id)) {
// 4. 主动抛出特定类型的错误
const err = new TypeError("用户 ID 必须是数字")
err.status = 400 // 添加状态码
return next(err)
}
res.send(`用户信息 ${id}`)
})
// 5. 全局错误处理中间件 (必须放在所有路由和中间件之后)
app.use((err, req, res, next) => {
// 记录错误
logger.error(
`${err.status || 500} - ${err.message} - ${req.originalUrl} - ${req.method} - ${
req.ip
}`
)
// 响应客户端
res.status(err.status || 500).json({
error: {
message: err.message || "服务器内部错误"
}
})
})
const PORT = 3000
app.listen(PORT, () => {
logger.info(`服务器运行在 http://localhost:${PORT}`)
})
// 6. 处理未捕获的全局异常
process.on("unhandledRejection", (reason, promise) => {
logger.error("未处理的 Promise 拒绝:", reason)
// 考虑关闭服务器
})
process.on("uncaughtException", (err) => {
logger.error("未捕获的同步异常:", err)
process.exit(1)
})常见问题解答
Q1: try...catch 能捕获异步错误吗?
A: 不能直接捕获。try...catch 只能捕获同步错误。对于异步错误,需要:
// ❌ 错误:无法捕获 Promise 拒绝
try {
Promise.reject(new Error('异步错误'))
} catch (error) {
// 不会执行
console.log(error)
}
// ✅ 方法1:使用 .catch()
Promise.reject(new Error('异步错误'))
.catch(error => console.log(error))
// ✅ 方法2:使用 async/await
async function handleAsync() {
try {
await Promise.reject(new Error('异步错误'))
} catch (error) {
console.log(error)
}
}Q2: 如何区分可恢复错误和不可恢复错误?
A: 通过 isOperational 标志区分:
// 可恢复错误(业务错误)
const operationalError = new ValidationError('参数错误')
operationalError.isOperational = true // 通常在自定义错误类中设置
// 不可恢复错误(编程错误)
const programmerError = new TypeError('undefined is not a function')
// 在全局错误处理中区分
app.use((err, req, res, next) => {
if (err.isOperational) {
// 可恢复错误:返回友好的错误信息
res.status(err.statusCode || 500).json({
error: err.message
})
} else {
// 不可恢复错误:记录详细信息,返回通用错误
logger.error('不可恢复错误:', err)
res.status(500).json({
error: '服务器内部错误'
})
// 考虑重启进程
process.exit(1)
}
})Q3: 应该在何时使用 uncaughtException?
A: 仅作为最后的防线,用于:
- 记录错误日志
- 清理资源(关闭数据库连接、文件句柄)
- 优雅关闭服务器
- 然后退出进程
process.on('uncaughtException', (error) => {
// 1. 记录错误
logger.error('未捕获异常:', error)
// 2. 关闭服务器,不再接受新请求
server.close(() => {
logger.info('服务器已关闭')
process.exit(1)
})
// 3. 强制退出(防止服务器关闭超时)
setTimeout(() => {
logger.error('强制退出')
process.exit(1)
}, 5000)
})Q4: 如何处理回调地狱中的错误?
A: 逐步迁移到 Promise 或 async/await:
// ❌ 回调地狱,难以处理错误
fs.readFile('file1.txt', 'utf8', (err1, data1) => {
if (err1) throw err1
fs.readFile('file2.txt', 'utf8', (err2, data2) => {
if (err2) throw err2
fs.writeFile('output.txt', data1 + data2, (err3) => {
if (err3) throw err3
console.log('完成')
})
})
})
// ✅ 使用 Promise
const fs = require('fs').promises
async function mergeFiles() {
try {
const [data1, data2] = await Promise.all([
fs.readFile('file1.txt', 'utf8'),
fs.readFile('file2.txt', 'utf8')
])
await fs.writeFile('output.txt', data1 + data2)
console.log('完成')
} catch (error) {
console.error('操作失败:', error)
}
}Q5: 如何在生产环境中优雅地处理错误?
A: 实施完整的错误处理策略:
-
分层错误处理:
- 数据层:包装数据库错误
- 业务层:验证业务逻辑,抛出业务错误
- 控制层:处理 HTTP 请求,返回合适的错误响应
- 全局层:捕获未处理错误,记录日志
-
错误监控:
- 集成错误追踪服务(Sentry、Bugsnag)
- 设置错误告警和通知
-
降级策略:
- 返回缓存数据
- 提供默认值
- 熔断和限流
-
文档和报告:
- 记录错误上下文
- 生成错误报告
- 定期审查和改进
Q6: 如何测试错误处理逻辑?
A: 使用单元测试和集成测试:
const assert = require('assert')
const { expect } = require('chai')
// 测试同步错误
describe('divide', () => {
it('应该在除数为零时抛出错误', () => {
assert.throws(
() => divide(10, 0),
Error,
'除数不能为零'
)
})
})
// 测试异步错误
describe('getUserById', () => {
it('应该在用户不存在时抛出 NotFoundError', async () => {
try {
await getUserById(999)
// 如果没有抛出错误,测试失败
throw new Error('应该抛出错误')
} catch (error) {
expect(error).to.be.instanceOf(NotFoundError)
expect(error.message).to.equal('用户不存在')
}
})
// 使用 Chai 的异步断言
it('应该在用户不存在时抛出 NotFoundError (优化)', async () => {
await expect(getUserById(999))
.to.be.rejectedWith(NotFoundError)
})
})异常处理流程图
┌─────────────────────────────────────────────────────────────┐
│ 错误发生 │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────┐
│ 是同步错误吗? │
└──────────┬──────────┘
│
┌─────────────┴─────────────┐
│ 是 │ 否
▼ ▼
┌───────────────┐ ┌─────────────────┐
│ try...catch │ │ Promise/async │
│ 捕获并处理 │ │ await 处理 │
└───────┬───────┘ └────────┬────────┘
│ │
│ ▼
│ ┌─────────────────┐
│ │ 是否有 .catch │
│ │ 或 try...catch? │
│ └────────┬────────┘
│ │
│ ┌─────────────┴─────────────┐
│ │ 是 │ 否
│ ▼ ▼
│ ┌─────────────┐ ┌─────────────────┐
│ │ 处理错误 │ │ unhandled │
│ └──────┬──────┘ │ Rejection 事件 │
│ │ └────────┬────────┘
│ │ │
└─────────────┴──────────────────────────┘
│
▼
┌─────────────────────┐
│ 错误是否可恢复? │
└──────────┬──────────┘
│
┌─────────────┴─────────────┐
│ 是 │ 否
▼ ▼
┌───────────────┐ ┌─────────────────┐
│ 重试/降级 │ │ 记录日志 │
│ 继续运行 │ │ 优雅关闭进程 │
└───────────────┘ └─────────────────┘最佳实践清单
✅ 应该做的
- 使用自定义错误类型区分不同类型的错误
- 在所有异步操作中使用
try...catch或.catch() - 记录错误的完整上下文信息(堆栈、参数、环境)
- 为异步操作设置合理的超时时间
- 实现重试机制处理临时性错误
- 使用断路器模式防止故障传播
- 对输入参数进行验证,主动抛出错误
- 集成错误监控服务(Sentry、Bugsnag)
- 编写测试覆盖错误场景
- 定期审查错误日志,持续改进
❌ 不应该做的
- 捕获错误后不做任何处理(
catch (e) {}) - 使用
uncaughtException作为主要错误处理机制 - 在生产环境中输出敏感信息(密码、密钥)
- 忽略 Promise 拒绝
- 混用多种错误处理模式
- 抛出字符串或其他非 Error 对象
- 在错误处理中执行复杂的异步操作
- 过度依赖全局错误处理
总结
Node.js 异常处理是一个系统性工程,需要从多个层面考虑:
- 预防阶段: 使用类型系统、Linter、单元测试
- 捕获阶段:
try...catch、Promise、事件监听 - 处理阶段: 重试、降级、熔断、超时
- 记录阶段: 结构化日志、错误追踪服务
- 改进阶段: 错误分析、优化代码、完善文档
一个健壮的错误处理系统能够:
- 快速定位和修复问题
- 提升用户体验
- 降低运维成本
- 提高系统可靠性
记住:错误处理不是事后补救,而是设计的一部分。在编码阶段就应该考虑可能的错误场景,并制定相应的处理策略。
Node.js 22+ 错误处理新特性
权限模型错误
Node.js 22.13+ 权限模型稳定后,新增了权限相关的错误类型:
process.on('uncaughtException', (err) => {
if (err.code === 'ERR_ACCESS_DENIED') {
console.error('权限被拒绝:', err.permission, err.resource)
}
})# 启用权限模型后,未授权操作会抛出 ERR_ACCESS_DENIED
node --permission --allow-fs-read=./data app.js
# Error: Access to this API has been restricted
# code: 'ERR_ACCESS_DENIED',
# permission: 'FileSystemRead',
# resource: '/etc/passwd'node:test 中的错误处理
Node.js 22+ 内置测试框架提供了更好的错误处理和断言:
import { describe, it, beforeEach, afterEach } from 'node:test'
import assert from 'node:assert/strict'
describe('用户服务', () => {
it('应该抛出 NotFoundError', async () => {
await assert.rejects(
() => getUserById(999),
(err) => {
assert.strictEqual(err.name, 'NotFoundError')
assert.strictEqual(err.statusCode, 404)
return true
}
)
})
it('应该匹配错误消息', () => {
assert.throws(
() => { throw new Error('无效输入') },
{ message: /无效/ }
)
})
})--watch 模式下的错误恢复
Node.js 22+ 的 --watch 模式在应用崩溃后会自动重启:
# 应用崩溃后自动重启
node --watch app.js
# 配合 --env-file 使用
node --watch --env-file=.env.development app.jsError.cause 错误链
Node.js 16.9+ 支持错误链,可在新错误中保留原始错误(已在前面章节介绍),Node.js 22+ 中更广泛使用:
// 内置模块也使用 Error.cause
try {
await fs.promises.readFile('config.json')
} catch (err) {
throw new Error('配置文件加载失败', { cause: err })
}
// 遍历完整错误链
function getErrorChain(error) {
const chain = []
let current = error
while (current) {
chain.push({
name: current.name,
message: current.message,
stack: current.stack?.split('\n')[0]
})
current = current.cause
}
return chain
}