如何構建 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 的錯誤處理