原文: How to Build a Node.js Error-handling System
不难看出,有些人正在努力处理错误,有些人甚至完全忽略了它。正确处理错误不仅意味着通过轻松查找错误和错误来减少开发时间,而且还意味着为大型应用程序开发强大的代码库。
特别是,Node.js 开发人员有时会发现自己在处理各种错误时使用不那么干净的代码,错误地在各处应用相同的逻辑来处理它们。他们只是不断地问自己 “Node.js 不擅长处理错误吗?” 或者如果没有,该如何处理?” 我对他们的回答是 “不,Node.js 一点也不差。这取决于我们开发商。”
这是我最喜欢的解决方案之一。
Node.js 错误处理:错误类型
首先,要对Node.js中的错误有一个清晰的认识。一般来说,Node.js 错误分为两个不同的类别:操作错误和程序员错误。
- 操作错误代表运行时问题,其结果是预期的,应该以正确的方式处理。操作错误并不意味着应用程序本身有bug,而是开发者需要深思熟虑地处理。操作错误的示例包括“内存不足”、“API 端点的输入无效”等。
- 程序员错误表示编写得不好的代码中出现的意外错误。他们的意思是代码本身有一些问题需要解决并且编码错误。一个很好的例子是尝试读取“未定义”的属性。要解决该问题,必须更改代码。这是开发人员犯的错误,而不是操作错误。
考虑到这一点,区分这两类错误应该没有问题:操作错误是应用程序的自然组成部分,而程序员错误是开发人员引起的错误。接下来的一个逻辑问题是: “为什么将它们分为两类并处理它们有用?”
如果没有清楚地了解错误,您可能会在发生错误时想要重新启动应用程序。当成千上万的用户正在使用该应用程序时,由于“找不到文件”错误而重新启动该应用程序是否有意义?绝对不。
但是程序员犯错误怎么办?当出现可能导致应用程序出现意外滚雪球效应的未知错误时,保持应用程序运行是否有意义?再次强调,绝对不是!
是时候正确处理错误了
假设您有一些使用异步 JavaScript 和 Node.js 的经验,那么在使用回调处理错误时您可能会遇到一些缺点。它们迫使您检查错误一直到嵌套错误,从而导致臭名昭著的“回调地狱”问题,从而难以遵循代码流。
使用 Promise 或 async/await 是回调的一个很好的替代品。async/await 的典型代码流程如下:
const doAsyncJobs = async () => {
try {
const result1 = await job1();
const result2 = await job2(result1);
const result3 = await job3(result2);
return await job4(result3);
} catch (error) {
console.error(error);
} finally {
await anywayDoThisJob();
}
}
使用 Node.js 内置 Error 对象是一种很好的做法,因为它包含有关错误的直观且清晰的信息,例如 StackTrace,大多数开发人员依靠它来跟踪错误的根源。其他有意义的属性(例如 HTTP 状态代码和通过扩展 Error 类进行的描述)将使其提供更多信息。
class BaseError extends Error {
public readonly name: string;
public readonly httpCode: HttpStatusCode;
public readonly isOperational: boolean;
constructor(name: string, httpCode: HttpStatusCode, description: string, isOperational: boolean) {
super(description);
Object.setPrototypeOf(this, new.target.prototype);
this.name = name;
this.httpCode = httpCode;
this.isOperational = isOperational;
Error.captureStackTrace(this);
}
}
//free to extend the BaseError
class APIError extends BaseError {
constructor(name, httpCode = HttpStatusCode.INTERNAL_SERVER, isOperational = true, description = 'internal server error') {
super(name, httpCode, isOperational, description);
}
}
为了简单起见,我只实现了一些 HTTP 状态代码,但您可以稍后添加更多状态代码。
export enum HttpStatusCode {
OK = 200,
BAD_REQUEST = 400,
NOT_FOUND = 404,
INTERNAL_SERVER = 500,
}
不需要扩展BaseError或者APIError,但是对于常见的错误根据自己的需求和个人喜好进行扩展是可以的。
class HTTP400Error extends BaseError {
constructor(description = 'bad request') {
super('NOT FOUND', HttpStatusCode.BAD_REQUEST, true, description);
}
}
那么如何使用它呢?只需将其放入:
...
const user = await User.getUserById(1);
if (user === null)
throw new APIError(
'NOT FOUND',
HttpStatusCode.NOT_FOUND,
true,
'detailed explanation'
);
集中式 Node.js 错误处理
现在,我们准备构建 Node.js 错误处理系统的主要组件:集中式错误处理组件。
构建集中式错误处理组件通常是一个好主意,以避免处理错误时可能出现的代码重复。错误处理组件负责使捕获的错误易于理解,例如,向系统管理员发送通知(如有必要)、将事件传输到 Sentry.io 等监控服务并记录它们。
以下是处理错误的基本工作流程:
在代码的某些部分,错误被捕获并传输到错误处理中间件。
...
try {
userService.addNewUser(req.body).then((newUser: User) => {
res.status(200).json(newUser);
}).catch((error: Error) => {
next(error)
});
} catch (error) {
next(error);
}
...
错误处理中间件是区分错误类型并将它们发送到集中式错误处理组件的好地方。了解有关 Express 错误处理的基础知识肯定会有所帮助。(还值得深入研究使用 Promises 的Express 错误处理最佳实践。)
app.use(async (err: Error, req: Request, res: Response, next: NextFunction) => {
if (!errorHandler.isTrustedError(err)) {
next(err);
}
await errorHandler.handleError(err);
});
现在我们已经可以想象中心化组件应该是什么样子了,因为我们已经使用了它的一些功能。请记住,如何实现它完全取决于您,但它可能如下所示:
class ErrorHandler {
public async handleError(err: Error): Promise<void> {
await logger.error(
'Error message from the centralized error-handling component',
err,
);
await sendMailToAdminIfCritical();
await sendEventsToSentry();
}
public isTrustedError(error: Error) {
if (error instanceof BaseError) {
return error.isOperational;
}
return false;
}
}
export const errorHandler = new ErrorHandler();
有时,默认“console.log”的输出使得跟踪错误变得困难。相反,最好以格式化的方式打印错误,以便开发人员可以快速了解问题并确保它们得到修复。
总体而言,这将节省开发人员的时间,使其能够轻松跟踪错误并通过提高可见性来处理错误。使用像winston或morgan这样的可定制记录器是一个不错的决定。
这是一个定制的日志记录器:
const customLevels = {
levels: {
trace: 5,
debug: 4,
info: 3,
warn: 2,
error: 1,
fatal: 0,
},
colors: {
trace: 'white',
debug: 'green',
info: 'green',
warn: 'yellow',
error: 'red',
fatal: 'red',
},
};
const formatter = winston.format.combine(
winston.format.colorize(),
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.splat(),
winston.format.printf((info) => {
const { timestamp, level, message, ...meta } = info;
return `${timestamp} [${level}]: ${message} ${
Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''
}`;
}),
);
class Logger {
private logger: winston.Logger;
constructor() {
const prodTransport = new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
});
const transport = new winston.transports.Console({
format: formatter,
});
this.logger = winston.createLogger({
level: isDevEnvironment() ? 'trace' : 'error',
levels: customLevels.levels,
transports: [isDevEnvironment() ? transport : prodTransport],
});
winston.addColors(customLevels.colors);
}
trace(msg: any, meta?: any) {
this.logger.log('trace', msg, meta);
}
debug(msg: any, meta?: any) {
this.logger.debug(msg, meta);
}
info(msg: any, meta?: any) {
this.logger.info(msg, meta);
}
warn(msg: any, meta?: any) {
this.logger.warn(msg, meta);
}
error(msg: any, meta?: any) {
this.logger.error(msg, meta);
}
fatal(msg: any, meta?: any) {
this.logger.log('fatal', msg, meta);
}
}
export const logger = new Logger();
它基本上提供的是以格式化的方式记录多个不同级别的日志,颜色清晰,并根据运行时环境记录到不同的输出介质。这样做的好处是您可以使用 Winston 的内置 API 来观察和查询日志。此外,您可以使用日志分析工具来分析格式化的日志文件,以获得有关应用程序的更多有用信息。太棒了,不是吗?
到目前为止,我们主要讨论了处理操作错误。程序员犯错误怎么办? 处理这些错误的最佳方法是立即崩溃并使用 PM2 等自动重启程序正常重启 - 原因是程序员错误是意外的,因为它们是实际的错误,可能会导致应用程序最终处于错误的状态和行为以一种意想不到的方式。
process.on('uncaughtException', (error: Error) => {
errorHandler.handleError(error);
if (!errorHandler.isTrustedError(error)) {
process.exit(1);
}
});
最后但并非最不重要的一点是,我将提到处理未处理的承诺拒绝和异常。
在开发 Node.js/Express 应用程序时,您可能会发现自己花费了大量时间来处理 Promise。当您忘记处理拒绝时,不难看到有关未处理的承诺拒绝的警告消息。
警告消息除了记录之外没有太多作用,但使用适当的回退并订阅process.on(‘unhandledRejection’, callback). 您可以将其视为一种 Node.js 全局错误处理程序。
典型的错误处理流程可能如下所示:
// somewhere in the code
...
User.getUserById(1).then((firstUser) => {
if (firstUser.isSleeping === false) throw new Error('He is not sleeping!');
});
...
// get the unhandled rejection and throw it to another fallback handler we already have.
process.on('unhandledRejection', (reason: Error, promise: Promise<any>) => {
throw reason;
});
process.on('uncaughtException', (error: Error) => {
errorHandler.handleError(error);
if (!errorHandler.isTrustedError(error)) {
process.exit(1);
}
});
Node.js 中的错误处理:必备
总而言之,您应该意识到错误处理不是可选的额外内容,而是应用程序的重要组成部分,无论是在开发阶段还是在生产中。
在 Node.js 中处理单个组件中的错误的策略将确保开发人员通过避免代码重复和丢失错误上下文来节省宝贵的时间并编写干净且可维护的代码。
我希望您喜欢阅读本文,并发现所讨论的错误处理工作流程和实现有助于在 Node.js 中构建强大的代码库。