Node.js 错误处理与调试
学习目标
- 理解 Node.js 中的错误类型
- 掌握错误捕获与处理的方法
- 学会创建和使用自定义错误
- 能够实现错误处理中间件
- 熟悉 Node.js 调试工具和技巧
错误处理基础
什么是错误?
错误是程序执行过程中遇到的异常情况,可能导致程序无法正常运行。在 Node.js 中,错误通常是 Error 对象的实例。
错误的重要性
- 提高代码健壮性:合理的错误处理可以防止程序崩溃
- 提升用户体验:友好的错误提示可以提高用户体验
- 便于调试:详细的错误信息有助于快速定位问题
- 增强系统可靠性:即使遇到错误,系统也能优雅降级
Node.js 中的错误类型
1. 内置错误类型
| 错误类型 | 描述 | 示例 |
|---|---|---|
| Error | 所有错误的基类 | new Error('通用错误') |
| SyntaxError | 语法错误 | const a = ; |
| ReferenceError | 引用错误 | console.log(undefinedVariable) |
| TypeError | 类型错误 | 'string'.map() |
| RangeError | 范围错误 | const arr = new Array(-1) |
| URIError | URI 错误 | decodeURI('%') |
| EvalError | eval() 错误 | 很少见,已被废弃 |
2. 系统错误
系统错误是由底层操作系统或 Node.js 运行时产生的错误,通常与 I/O 操作相关。
const fs = require('fs');
// 文件不存在错误
fs.readFile('不存在的文件.txt', (err, data) => {
if (err) {
console.error('系统错误:', err);
// Error: ENOENT: no such file or directory, open '不存在的文件.txt'
}
});3. 自定义错误
开发者可以创建自定义错误类型来表示特定的业务逻辑错误。
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
this.statusCode = 400;
}
}
// 使用自定义错误
function validateUser(user) {
if (!user.name) {
throw new ValidationError('用户名不能为空', 'name');
}
if (!user.email) {
throw new ValidationError('邮箱不能为空', 'email');
}
}错误捕获与处理
1. try/catch 语句
用于捕获同步代码中的错误。
try {
// 可能抛出错误的代码
const result = JSON.parse('invalid json');
console.log(result);
} catch (error) {
// 错误处理
console.error('捕获到错误:', error.message);
// 捕获到错误: Unexpected token i in JSON at position 0
}2. 回调函数中的错误处理
在 Node.js 早期,回调函数是处理异步操作的主要方式,通常使用错误优先的回调模式。
const fs = require('fs');
// 错误优先回调
fs.readFile('file.txt', (err, data) => {
if (err) {
// 错误处理
console.error('读取文件失败:', err);
return;
}
// 成功处理
console.log('文件内容:', data.toString());
});3. Promise 中的错误处理
使用 .catch() 方法捕获 Promise 链中的错误。
const fs = require('fs').promises;
fs.readFile('file.txt')
.then(data => {
console.log('文件内容:', data.toString());
return JSON.parse('invalid json'); // 这里会抛出错误
})
.catch(error => {
console.error('捕获到错误:', error.message);
});4. async/await 中的错误处理
结合 try/catch 捕获 async/await 中的错误。
const fs = require('fs').promises;
async function readFile() {
try {
const data = await fs.readFile('file.txt');
console.log('文件内容:', data.toString());
const result = JSON.parse('invalid json'); // 这里会抛出错误
return result;
} catch (error) {
console.error('捕获到错误:', error.message);
// 可以选择重新抛出错误
// throw error;
}
}
readFile();自定义错误
创建自定义错误类
继承自 Error 类创建自定义错误。
class AppError extends Error {
constructor(message, statusCode, isOperational = true) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.isOperational = isOperational;
// 捕获堆栈跟踪
Error.captureStackTrace(this, this.constructor);
}
}
// 使用自定义错误
function divide(a, b) {
if (b === 0) {
throw new AppError('除数不能为零', 400);
}
return a / b;
}
try {
const result = divide(10, 0);
console.log(result);
} catch (error) {
console.error('错误状态码:', error.statusCode);
console.error('错误消息:', error.message);
console.error('错误状态:', error.status);
}业务逻辑错误
为特定业务场景创建自定义错误。
// 验证错误
class ValidationError extends AppError {
constructor(message, field) {
super(message, 400);
this.field = field;
this.name = 'ValidationError';
}
}
// 认证错误
class AuthError extends AppError {
constructor(message) {
super(message, 401);
this.name = 'AuthError';
}
}
// 权限错误
class ForbiddenError extends AppError {
constructor(message) {
super(message, 403);
this.name = 'ForbiddenError';
}
}
// 资源不存在错误
class NotFoundError extends AppError {
constructor(message) {
super(message, 404);
this.name = 'NotFoundError';
}
}错误处理中间件
Express 错误处理中间件
在 Express 应用中,错误处理中间件可以捕获和处理路由处理函数中抛出的错误。
const express = require('express');
const app = express();
// 普通中间件
app.use(express.json());
// 路由
app.get('/api/users/:id', (req, res) => {
const { id } = req.params;
if (id === '0') {
throw new AppError('无效的用户 ID', 400);
}
if (id === '100') {
throw new NotFoundError('用户不存在');
}
res.json({ id, name: '张三', email: 'zhangsan@example.com' });
});
// 错误处理中间件(四个参数)
app.use((err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
res.status(err.statusCode).json({
status: err.status,
message: err.message,
// 在开发环境中显示错误堆栈
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
});
app.listen(3000, () => {
console.log('服务器运行在 http://localhost:3000');
});全局错误处理
对于未被捕获的错误,可以使用全局错误处理器。
// 捕获未处理的 Promise 拒绝
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise 拒绝:', reason);
// 可以在这里进行日志记录、告警等操作
});
// 捕获未捕获的异常
process.on('uncaughtException', (error) => {
console.error('未捕获的异常:', error);
// 注意:在生产环境中,应该优雅地关闭服务器
// process.exit(1); // 非零退出码表示错误
});调试工具与技巧
1. console 方法
最简单的调试方法是使用 console 对象的方法。
// 打印普通信息
console.log('调试信息:', variable);
// 打印警告信息
console.warn('警告信息');
// 打印错误信息
console.error('错误信息');
// 打印对象的详细信息
console.dir(object, { depth: null });
// 打印调用栈
console.trace('调用栈');
// 计算执行时间
console.time('计时器');
// 执行代码
console.timeEnd('计时器');2. Node.js 调试器
Node.js 内置了调试器,可以通过 --inspect 或 --inspect-brk 选项启动。
启动调试模式
# 启动调试模式
node --inspect app.js
# 启动调试模式并在第一行暂停
node --inspect-brk app.js使用 Chrome DevTools 调试
- 启动调试模式后,打开 Chrome 浏览器
- 访问
chrome://inspect - 在 "Remote Target" 部分找到你的 Node.js 应用
- 点击 "inspect" 开始调试
3. VS Code 调试
VS Code 内置了强大的 Node.js 调试功能。
配置调试
- 在 VS Code 中打开项目
- 点击左侧的调试图标(或按
Ctrl+Shift+D) - 点击 "创建 launch.json 文件"
- 选择 "Node.js" 环境
调试配置示例
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "启动程序",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/app.js"
},
{
"type": "node",
"request": "attach",
"name": "附加到进程",
"processId": "${command:PickProcess}"
}
]
}调试技巧
- 设置断点:点击代码行号左侧的空白区域
- 条件断点:右键点击断点,设置条件
- 监视变量:在监视面板中添加变量
- 单步执行:使用 F10(单步跳过)、F11(单步进入)、Shift+F11(单步退出)
- 查看调用栈:在调用栈面板中查看函数调用链
4. 第三方调试工具
ndb
ndb 是 Google 开发的增强型 Node.js 调试工具,基于 Chrome DevTools。
# 安装
npm install -g ndb
# 使用
ndb app.jsdebug 模块
debug 模块是一个轻量级的调试工具,可以根据环境变量控制调试信息的输出。
# 安装
npm install debug// 使用 debug 模块
const debug = require('debug')('app:server');
const dbDebug = require('debug')('app:db');
// 启动服务器
debug('服务器启动在端口 3000');
// 数据库操作
dbDebug('连接到数据库');# 运行时指定调试命名空间
DEBUG=app:server node app.js
# 运行时指定多个调试命名空间
DEBUG=app:server,app:db node app.js
# 运行时指定所有调试命名空间
DEBUG=* node app.js实战案例:实现错误处理中间件
项目结构
error-handling-example/
├── src/
│ ├── errors/
│ │ └── appError.js # 自定义错误类
│ ├── middleware/
│ │ └── errorHandler.js # 错误处理中间件
│ ├── routes/
│ │ └── userRoutes.js # 用户路由
│ └── app.js # 应用入口
├── package.json
└── README.md实现步骤
- 创建自定义错误类
// src/errors/appError.js
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);
}
}
module.exports = AppError;- 实现错误处理中间件
// src/middleware/errorHandler.js
const AppError = require('../errors/appError');
const handleCastErrorDB = (err) => {
const message = `无效的 ${err.path}: ${err.value}`;
return new AppError(message, 400);
};
const handleDuplicateFieldsDB = (err) => {
const value = err.errmsg.match(/\"([^\"]*)\"/)[1];
const message = `重复的字段值: ${value}。请使用其他值`;
return new AppError(message, 400);
};
const handleValidationErrorDB = (err) => {
const errors = Object.values(err.errors).map(el => el.message);
const message = `验证失败: ${errors.join('. ')}`;
return new AppError(message, 400);
};
const handleJWTError = () => {
return new AppError('无效的令牌。请重新登录', 401);
};
const handleJWTExpiredError = () => {
return new AppError('令牌已过期。请重新登录', 401);
};
const sendErrorDev = (err, res) => {
res.status(err.statusCode).json({
status: err.status,
error: err,
message: err.message,
stack: err.stack
});
};
const sendErrorProd = (err, res) => {
// 只向客户端发送可操作的错误
if (err.isOperational) {
res.status(err.statusCode).json({
status: err.status,
message: err.message
});
} else {
// 记录错误
console.error('错误:', err);
// 发送通用错误消息
res.status(500).json({
status: 'error',
message: '服务器内部错误'
});
}
};
module.exports = (err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
if (process.env.NODE_ENV === 'development') {
sendErrorDev(err, res);
} else if (process.env.NODE_ENV === 'production') {
let error = { ...err };
error.message = err.message;
// 处理数据库错误
if (err.name === 'CastError') error = handleCastErrorDB(error);
if (err.code === 11000) error = handleDuplicateFieldsDB(error);
if (err.name === 'ValidationError') error = handleValidationErrorDB(error);
// 处理 JWT 错误
if (err.name === 'JsonWebTokenError') error = handleJWTError();
if (err.name === 'TokenExpiredError') error = handleJWTExpiredError();
sendErrorProd(error, res);
}
};- 创建用户路由
// src/routes/userRoutes.js
const express = require('express');
const router = express.Router();
const AppError = require('../errors/appError');
// 模拟用户数据
const users = [
{ id: 1, name: '张三', email: 'zhangsan@example.com' },
{ id: 2, name: '李四', email: 'lisi@example.com' },
{ id: 3, name: '王五', email: 'wangwu@example.com' }
];
// 获取所有用户
router.get('/', (req, res) => {
res.json({ status: 'success', data: { users } });
});
// 获取单个用户
router.get('/:id', (req, res, next) => {
const { id } = req.params;
const user = users.find(u => u.id === parseInt(id));
if (!user) {
return next(new AppError('用户不存在', 404));
}
res.json({ status: 'success', data: { user } });
});
// 创建用户
router.post('/', (req, res, next) => {
const { name, email } = req.body;
if (!name || !email) {
return next(new AppError('姓名和邮箱不能为空', 400));
}
const newUser = {
id: users.length + 1,
name,
email
};
users.push(newUser);
res.status(201).json({ status: 'success', data: { user: newUser } });
});
// 更新用户
router.patch('/:id', (req, res, next) => {
const { id } = req.params;
const { name, email } = req.body;
const user = users.find(u => u.id === parseInt(id));
if (!user) {
return next(new AppError('用户不存在', 404));
}
if (name) user.name = name;
if (email) user.email = email;
res.json({ status: 'success', data: { user } });
});
// 删除用户
router.delete('/:id', (req, res, next) => {
const { id } = req.params;
const index = users.findIndex(u => u.id === parseInt(id));
if (index === -1) {
return next(new AppError('用户不存在', 404));
}
users.splice(index, 1);
res.json({ status: 'success', data: null });
});
module.exports = router;- 创建应用入口
// src/app.js
const express = require('express');
const userRoutes = require('./routes/userRoutes');
const errorHandler = require('./middleware/errorHandler');
const AppError = require('./errors/appError');
const app = express();
// 中间件
app.use(express.json());
// 路由
app.use('/api/users', userRoutes);
// 处理未定义的路由
app.all('*', (req, res, next) => {
next(new AppError(`无法找到 ${req.originalUrl} 路径`, 404));
});
// 错误处理中间件
app.use(errorHandler);
// 全局错误处理
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise 拒绝:', reason);
// 可以在这里进行日志记录、告警等操作
});
process.on('uncaughtException', (error) => {
console.error('未捕获的异常:', error);
// 注意:在生产环境中,应该优雅地关闭服务器
// process.exit(1);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});常见问题与解决方案
1. 如何区分可操作错误和编程错误?
- 可操作错误:由用户输入或系统状态引起的错误,如无效的请求参数、数据库连接失败等
- 编程错误:由代码逻辑错误引起的错误,如引用未定义变量、调用不存在的方法等
2. 如何处理异步错误?
- 回调函数:使用错误优先的回调模式
- Promise:使用
.catch()方法 - async/await:结合
try/catch语句 - 全局处理器:使用
process.on('unhandledRejection')捕获未处理的 Promise 拒绝
3. 如何避免错误处理中间件被跳过?
- 确保在所有路由之后定义错误处理中间件
- 在路由处理函数中使用
next(err)传递错误 - 不要在错误处理中间件之前使用
res.send()或res.json()等方法
4. 如何在生产环境中保护错误信息?
- 生产环境中不要向客户端暴露详细的错误堆栈
- 使用不同的错误响应格式(开发环境 vs 生产环境)
- 对错误进行分类,只向客户端暴露可操作的错误
代码优化建议
- 统一错误处理:使用集中式的错误处理中间件
- 详细的错误信息:在开发环境中提供详细的错误信息
- 适当的错误日志:记录错误信息以便排查问题
- 错误分类:根据错误类型采取不同的处理策略
- 优雅降级:即使遇到错误,也应该尽可能保持系统可用
- 使用调试工具:合理使用调试工具提高开发效率
- 错误预防:在可能出错的地方添加防御性代码
学习总结
- 错误类型:内置错误类型、系统错误、自定义错误
- 错误捕获:try/catch、回调函数、Promise.catch()、async/await
- 自定义错误:继承自 Error 类创建自定义错误
- 错误处理中间件:Express 错误处理中间件、全局错误处理器
- 调试工具:console 方法、Node.js 调试器、VS Code 调试、第三方调试工具
- 调试技巧:设置断点、监视变量、单步执行、查看调用栈
动手练习
- 实现一个完整的错误处理系统,包括自定义错误类和错误处理中间件
- 使用 VS Code 调试器调试一个包含错误的 Node.js 应用
- 实现一个基于 debug 模块的日志系统
- 为一个 RESTful API 添加完善的错误处理
进阶学习
- 错误监控:使用 Sentry、LogRocket 等工具监控生产环境中的错误
- 性能分析:使用 Clinic.js 等工具分析应用性能
- 混沌工程:通过故意引入错误来测试系统的弹性
- 容错设计:设计具有容错能力的系统架构
- 分布式追踪:使用 OpenTelemetry 等工具进行分布式追踪
通过本教程的学习,你已经掌握了 Node.js 中的错误处理机制和调试技巧,能够编写更加健壮和可维护的代码。在实际开发中,合理的错误处理和调试技巧将大大提高你的开发效率和代码质量。