如何构建 Node.js 错误处理系统
已发表: 2022-03-11不难看出,有些人正在努力处理错误,有些人甚至完全错过了它。 正确处理错误不仅意味着通过轻松发现错误和错误来减少开发时间,而且还意味着为大型应用程序开发强大的代码库。
特别是,Node.js 开发人员有时会发现自己在处理各种错误时使用不那么干净的代码,错误地在各处应用相同的逻辑来处理它们。 他们只是不断地问自己“Node.js 不擅长处理错误吗?” 否则,如何处理?” 我对他们的回答是“不,Node.js 一点也不差。 这取决于我们的开发人员。”
这是我最喜欢的解决方案之一。
Node.js 中的错误类型
首先,需要对 Node.js 中的错误有一个清晰的认识。 一般来说,Node.js 错误分为两个不同的类别:操作错误和程序员错误。
- 操作错误代表运行时问题,其结果是预期的,应该以适当的方式处理。 操作错误并不意味着应用程序本身有错误,但开发人员需要深思熟虑地处理它们。 操作错误的示例包括“内存不足”、“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.js 中间件中处理错误的基础知识肯定会有所帮助。
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)
是一个好习惯。
典型的错误处理流程可能如下所示:
// 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 中构建健壮的代码库。
进一步阅读 Toptal 工程博客:
- 使用 Express.js 路由进行基于 Promise 的错误处理