Node.js 错误处理
问题
Node.js 中如何正确处理错误?同步和异步错误有什么区别?如何避免进程因未捕获异常而崩溃?
答案
错误处理是 Node.js 应用稳定性的关键。Node.js 中的错误分为同步错误和异步错误,需要不同的处理方式。
错误类型
内置错误类型
// Error - 基础错误
throw new Error('Something went wrong');
// TypeError - 类型错误
const a = null;
a.toString(); // TypeError: Cannot read property 'toString' of null
// RangeError - 范围错误
new Array(-1); // RangeError: Invalid array length
// ReferenceError - 引用错误
console.log(undefinedVar); // ReferenceError: undefinedVar is not defined
// SyntaxError - 语法错误(通常在解析时)
JSON.parse('{invalid}'); // SyntaxError: Unexpected token
// Node.js 特有
// SystemError - 系统调用错误
// 包含 errno、code、syscall 等属性
自定义错误
// 自定义错误类
class AppError extends Error {
code: string;
statusCode: number;
isOperational: boolean;
constructor(message: string, code: string, statusCode = 500) {
super(message);
this.name = 'AppError';
this.code = code;
this.statusCode = statusCode;
this.isOperational = true; // 可恢复的操作错误
Error.captureStackTrace(this, this.constructor);
}
}
// 业务错误
class ValidationError extends AppError {
constructor(message: string) {
super(message, 'VALIDATION_ERROR', 400);
}
}
class NotFoundError extends AppError {
constructor(resource: string) {
super(`${resource} not found`, 'NOT_FOUND', 404);
}
}
class UnauthorizedError extends AppError {
constructor() {
super('Unauthorized', 'UNAUTHORIZED', 401);
}
}
// 使用
throw new ValidationError('Email is required');
throw new NotFoundError('User');
同步错误处理
// try-catch
try {
JSON.parse('{invalid}');
} catch (error) {
if (error instanceof SyntaxError) {
console.error('JSON 解析错误:', error.message);
} else {
throw error; // 重新抛出未知错误
}
}
// 类型断言(TypeScript)
try {
doSomething();
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
console.error(error.stack);
}
}
异步错误处理
回调模式
import { readFile } from 'fs';
// Node.js 错误优先回调约定
readFile('file.txt', (err, data) => {
if (err) {
console.error('读取失败:', err.message);
return;
}
console.log(data);
});
// 自定义错误优先回调
function fetchData(callback: (err: Error | null, data?: any) => void) {
try {
const data = processData();
callback(null, data);
} catch (err) {
callback(err instanceof Error ? err : new Error(String(err)));
}
}
Promise 模式
import { readFile } from 'fs/promises';
// async/await + try-catch
async function loadConfig() {
try {
const data = await readFile('config.json', 'utf8');
return JSON.parse(data);
} catch (error) {
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
console.log('配置文件不存在,使用默认配置');
return {};
}
throw error;
}
}
// .catch()
readFile('file.txt')
.then((data) => console.log(data))
.catch((err) => console.error(err));
// Promise.allSettled(不会因一个失败而中断)
const results = await Promise.allSettled([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/comments')
]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`请求 ${index} 成功:`, result.value);
} else {
console.error(`请求 ${index} 失败:`, result.reason);
}
});
EventEmitter 错误
import { EventEmitter } from 'events';
const emitter = new EventEmitter();
// 必须监听 error 事件,否则会抛出
emitter.on('error', (err) => {
console.error('EventEmitter 错误:', err);
});
// 触发错误
emitter.emit('error', new Error('Something failed'));
// Stream 错误(Stream 是 EventEmitter)
import { createReadStream } from 'fs';
const stream = createReadStream('nonexistent.txt');
stream.on('error', (err) => {
console.error('Stream 错误:', err.message);
});
全局错误处理
uncaughtException
// 捕获未处理的同步异常
process.on('uncaughtException', (err, origin) => {
console.error('未捕获异常:', err);
console.error('来源:', origin);
// 记录日志
logger.error('uncaughtException', { error: err, origin });
// 优雅退出
process.exit(1);
});
// ⚠️ 注意:继续运行可能导致不确定状态
// 推荐:记录日志后退出进程,由进程管理器重启
unhandledRejection
// 捕获未处理的 Promise 拒绝
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise 拒绝:', reason);
// 记录日志
logger.error('unhandledRejection', { reason, promise });
// 可选:抛出错误触发 uncaughtException
// throw reason;
});
// Node.js 15+ 默认行为:未处理的 rejection 会导致进程退出
// 可通过 --unhandled-rejections=warn 修改
warning
// 捕获 Node.js 警告
process.on('warning', (warning) => {
console.warn('警告:', warning.name);
console.warn('信息:', warning.message);
console.warn('堆栈:', warning.stack);
});
// 触发警告
process.emitWarning('这是一个警告', {
code: 'MY_WARNING',
detail: '额外信息'
});
Express 错误处理
import express, { Request, Response, NextFunction } from 'express';
const app = express();
// 异步路由错误处理
const asyncHandler = (fn: Function) => {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
// 路由
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await findUser(req.params.id);
if (!user) {
throw new NotFoundError('User');
}
res.json(user);
}));
// 404 处理
app.use((req, res, next) => {
next(new NotFoundError(`Route ${req.url}`));
});
// 错误处理中间件(必须有 4 个参数)
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
// 记录日志
logger.error('Request error', {
error: err,
url: req.url,
method: req.method
});
// 返回错误响应
if (err instanceof AppError) {
res.status(err.statusCode).json({
success: false,
code: err.code,
message: err.message
});
} else {
// 未知错误,返回 500
res.status(500).json({
success: false,
code: 'INTERNAL_ERROR',
message: 'Internal server error'
});
}
});
日志记录
import winston from 'winston';
// 创建 logger
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
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()
}));
}
// 使用
logger.error('Database connection failed', {
error: new Error('Connection timeout'),
host: 'localhost',
port: 5432
});
logger.info('User logged in', {
userId: 123,
ip: '192.168.1.1'
});
常见面试问题
Q1: Node.js 中如何处理未捕获的异常?
答案:
// 1. uncaughtException - 同步异常
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
// 记录日志
// 优雅退出
process.exit(1);
});
// 2. unhandledRejection - Promise 拒绝
process.on('unhandledRejection', (reason) => {
console.error('Unhandled Rejection:', reason);
// 可选择抛出或记录
});
// 3. 域(已废弃,不推荐)
// const domain = require('domain');
// 最佳实践
// - 始终处理 Promise 错误
// - 使用 try-catch 包装同步代码
// - uncaughtException 后应该退出进程
// - 使用 PM2 等进程管理器自动重启
Q2: 操作错误和编程错误有什么区别?
答案:
操作错误(Operational Errors):
- 运行时预期可能发生的错误
- 可以优雅处理
- 示例:文件不存在、网络超时、用户输入无效
编程错误(Programmer Errors):
- 代码 bug
- 应该修复代码而非处理
- 示例:TypeError、undefined 访问、类型错误
// 操作错误 - 处理
try {
await readFile('config.json');
} catch (err) {
if (err.code === 'ENOENT') {
// 文件不存在,使用默认配置
return defaultConfig;
}
throw err;
}
// 编程错误 - 修复代码
const user = null;
console.log(user.name); // TypeError - 这是 bug
// 区分处理
class AppError extends Error {
isOperational: boolean;
constructor(message: string, isOperational = true) {
super(message);
this.isOperational = isOperational;
}
}
process.on('uncaughtException', (err) => {
if (err instanceof AppError && err.isOperational) {
// 操作错误,可以恢复
logger.error(err);
} else {
// 编程错误,退出
logger.error(err);
process.exit(1);
}
});
Q3: 为什么 uncaughtException 后应该退出进程?
答案:
未捕获异常后,应用可能处于不确定状态:
- 请求可能未完成
- 数据可能不一致
- 资源可能未释放
process.on('uncaughtException', async (err) => {
console.error('Uncaught Exception:', err);
// 记录日志
await logger.error(err);
// 关闭服务器,停止接受新请求
server.close(() => {
// 关闭数据库连接
db.close();
// 退出进程
process.exit(1);
});
// 如果关闭超时,强制退出
setTimeout(() => {
process.exit(1);
}, 10000);
});
// 使用 PM2 自动重启
// pm2 start app.js
Q4: async/await 中如何统一处理错误?
答案:
// 方式一:高阶函数包装
const asyncHandler = <T>(fn: () => Promise<T>) => {
return fn().catch((err) => {
console.error(err);
throw err;
});
};
// 方式二:Express 中间件包装
const wrapAsync = (fn: Function) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
app.get('/api', wrapAsync(async (req, res) => {
const data = await fetchData();
res.json(data);
}));
// 方式三:自定义结果类型
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
async function tryCatch<T>(
fn: () => Promise<T>
): Promise<Result<T>> {
try {
const data = await fn();
return { success: true, data };
} catch (error) {
return { success: false, error: error as Error };
}
}
// 使用
const result = await tryCatch(() => fetchUser(id));
if (!result.success) {
console.error(result.error);
}
Q5: 如何实现优雅退出?
答案:
import { createServer } from 'http';
const server = createServer(app);
let isShuttingDown = false;
// 优雅退出函数
async function gracefulShutdown(signal: string) {
console.log(`收到 ${signal},开始优雅退出...`);
isShuttingDown = true;
// 停止接受新请求
server.close(async () => {
console.log('HTTP 服务器已关闭');
// 关闭数据库连接
await db.close();
console.log('数据库连接已关闭');
// 关闭其他资源
await redis.quit();
console.log('Redis 连接已关闭');
process.exit(0);
});
// 超时强制退出
setTimeout(() => {
console.error('超时,强制退出');
process.exit(1);
}, 30000);
}
// 监听信号
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
// 中间件:拒绝新请求
app.use((req, res, next) => {
if (isShuttingDown) {
res.status(503).send('Server is shutting down');
return;
}
next();
});