如何優雅地在Node應用中進行錯誤處理 常用的錯誤處理機制 解決方案 總結 參考資料

不知道你有沒有遇到這樣一種情況,某天你寫的代碼在線上突然發生錯誤,然後你打開控制檯,卻對着打過包的錯誤信息毫無頭緒?又或者說是代碼在node端出現了問題,你查看錯誤日誌的時候,卻發現日誌文件中都是雜亂的錯誤堆棧信息。

其實上面這些問題都可以通過在代碼中引入合適的錯誤機制進行解決。大部分時候,由於程序員在開發過程中更加關注需求的實現,反而會忽視一些底層的工作。而錯誤處理機制就相當於我們代碼上的最後一道保險,在程序發生已知或者意外的問題的時候,可以讓開發者在第一時間獲取信息,從而快速定位並解決問題。

常用的錯誤處理機制

首先我們來了解一下目前前端領域到底有哪些錯誤處理機制。

try catch

try...catch這種錯誤處理機制一定是大家最熟悉的,Javascript語言內置的錯誤處理機制可以在檢測到代碼異常的時候直接進行捕獲並處理。

function test() {
  try {
    throw new Error("error");
  } catch(err) {
    console.log("some error happened:");
  }
}

test()

node原生錯誤處理機制

大多數Node.js核心API都提供的是利用回調函數處理錯誤,例如:

const fs = require('fs');

function read() {
    fs.readFile("/some/file/does-not-exist", (err, data) => {
    if(err) {
      throw new Error("file not exist");
    }
    console.log(data);
  });
}

read();

通過回調函數的err參數來檢查是否出現錯誤,再進行處理。之所以Node.js採用這種錯誤處理機制,是因爲異步方法所產生的方法並不能簡單地通過try...catch機制進行攔截。

promise

Promise是用於處理異步調用的規範,而其提供的錯誤處理機制,是通過catch方法進行捕獲。

fs.mkdir("./temp").then(() => {
    fs.writeFile("./temp/foobar.txt", "hello");
}).catch(err => {
    console.log(err)
});

async/await + try catch

第三種錯誤處理機制是採用async/await語法糖加上try...catch語句進行的。這樣做的好處是異步和同步調用都能夠使用統一的方式進行處理了。

async function one() {
    await two();
}

async function two() {
    await "hello";
    throw new Error("error");
}

async function test() {
    try {
        await one();
    } catch(error) {
        console.log(error);
    }
}

test();

解決方案

promisify

如果你的代碼中充斥着多種不同的錯誤處理模式,那麼維護起來是十分困難的。而且代碼的可讀性也會大大降低。因此,這裏推薦採用的統一的解決方案。對於同步代碼來說,直接使用try...catch方式進行捕獲處理即可,而對於異步代碼來說,建議轉換成Promise然後採用async/await + try...catch這種方式進行處理。這樣風格統一,程序的健壯性也大大加強。例如下面這個數據庫請求的代碼:

const database = require("database");

function promiseGet(query) {
    return new Promise((resolve, reject) => {
        database.get(query, (err, result) => {
            if (err) {
                reject(err);
            } else {
                resolve(result);
            }
        })
    })
}

async function main() {
    await promiseGet("foo");
}

main();

自定義錯誤類型

直接使用系統原生的錯誤信息通常會顯得太過單薄,不利於後續進一步的分析和處理。所以爲了讓代碼的錯誤處理機制的功能更加強大,我們勢必要多花點精力進行額外的改造。

可以通過擴展基礎的Error類型來達到這一目的。

一般來說,要根據錯誤發生的位置採用不同的錯誤類型。

首先是應用層錯誤,它會保存額外的線索數據:

class ApplicationError extends Error {
  constructor(message, options = {}) {
    assert(typeof message === 'string');
    assert(typeof options === 'object');
    assert(options !== null);
    super(message);

    // Attach relevant information to the error instance
    // (e.g., the username).
    for (const [key, value] of Object.entries(options)) {
      this[key] = value;
    }
  }

  get name() {
    return this.constructor.name;
  }
}

接着,可以再定義用戶接口的錯誤類型,該類型主要用於直接返回給客戶端,比如錯誤狀態碼等等。

class UserFacingError extends ApplicationError {
    constructor(message, options = {}) {
        super(message, options);
    }
}

class BadRequestError extends UserFacingError {
  get statusCode() {
    return 400
  }
}

class NotFoundError extends UserFacingError {
  get statusCode() {
    return 404
  }
}

另外,對於底層的數據庫錯誤來說,可能需要更加細節的錯誤信息。此時也可以根據業務需要進行自定義:

class DatabaseError extends ApplicationError {
  get toString() {
    return "Errored happend in query: " + this.query + "\n" + this.message;
  }
}

// 使用的話
throw new DatabaseError("Other message", {
  query: query
});

化繁爲簡,集中處理

express

有了基礎的錯誤數據類型後,我們可以在代碼裏針對不同的錯誤類型採取不同的解決方案。
接下來,以Express應用爲例講解一下使用方法。

app.use('/user', (req, res, next) => {
  const data = await database.getData(req.params.userId);
  if (!data) {
    throw new NotFoundError("User not found")
  }
  
  // do other thing
});

// 錯誤處理中間件
app.use(async (err, req, res, next) => {
  if (err instanceof UserFacingError) {
    res.sendStatus(err.statusCode);
    // or
    res.status(err.statusCode).send(err.errorCode)
  } else {
    res.sendStatus(500)
  }
  
  // 記錄日誌
  await logger.logError(err, 'parameter:', req.params, 'User Data:', req.user);
    // 發送郵件
  await sendMailToAdminIfCritical();
})

具體到實際場景中,需要在不同的路由中拋出不同的錯誤類型,然後我們就可以通過在錯誤處理中間件中進行統一的處理。比如根據不同的錯誤類型返回不同的錯誤碼。還可以進行記錄日誌,發送郵件等操作。

database

數據庫發生錯誤的時候,除了常規的拋出錯誤,有時候你可能還需要進行額外的重試或回退操作,如:

// 發生網絡錯誤的時候隔200ms,重試3次
function query(queryStr, token, repeatTime = 0, delay = 200) {
  try {
    await db.query(queryStr);
  } catch (err) {
    if (err instanceof NetworkError && repeatTime < 3) {
      query(queryStr, token, repeatTime + 1, delay);
    }
    
    throw err;
  }
}

未處理錯誤

對於未處理的錯誤來說,我們可以使用node.js的unhandledRejection事件進行監聽:

process.on('unhandledRejection', error => {

  console.error('unhandledRejection', error);
    // To exit with a 'failure' code
  process.exit(1);
});

而且從Node.js 12.0開始,可以使用以下命令啓動程序:

node app.js --unhandled-rejections

這樣也能夠在發現未處理異常的時候進行處理,官方支持了三種對應的處理模式:

strict: Raise the unhandled rejection as an uncaught exception.

warn: Always trigger a warning, no matter if the unhandledRejection hook is set or not but do not print the deprecation warning.

none: Silence all warnings.

總結

最後,總結一下。爲了實現可擴展和可維護的錯誤處理機制,我們可以需要注意以下幾個方面:

  • 使用自定義Error類,後續還能根據業務需要進行擴展
  • 將異步代碼轉換成Promise,然後統一使用async/await + try...catch的形式進行錯誤捕獲
  • 儘量採用統一的錯誤處理中間件函數
  • 保持Error信息可理解,返回合適的錯誤狀態和代碼
  • 對於未處理的錯誤,要即使捕獲並記錄

——--轉載請註明出處--———


最後,歡迎大家關注我的公衆號,一起學習交流。

參考資料

https://youtu.be/ArfAzp_bSq4
https://softwareontheroad.com/error-handling-nodejs/
https://github.com/goldbergyoni/nodebestpractices/blob/master/sections/errorhandling/centralizedhandling.md
https://michalzalecki.com/an-elegant-solution-for-handling-errors-in-express/
https://khalilstemmler.com/articles/enterprise-typescript-nodejs/functional-error-handling/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章