如何處理 Node.js 中出現的未捕獲異常?

liuxuhui發表於2021-09-09

圖片描述

Node.js 程式執行在單程式上,應用開發時一個難免遇到的問題就是異常處理,對於一些未捕獲的異常處理起來,也不是一件容易的事情。

未捕獲異常的程式

下面展示了一段簡單的應用程式,如下所示:

const http = require('http');
const PORT = 3000;
const server = http.createServer((req, res) => {
  if (req.url === '/error') {
    a.b;
    res.end('error');
  } else {
    setTimeout(() => res.end('ok!'), 1000 * 10);
  }
});

server.listen(PORT, () => console.log(`port is listening on ${PORT}.`));

執行以上程式,在右側第二個視窗中執行了 /error 路由,因為沒有定義 a 這個物件,則會引發錯誤。

圖片描述

程式崩潰退出之後導致整個應用程式也將崩潰,左側是一個延遲的響應,也將無法正常工作。

這是一個頭疼的問題,不要緊,下文我們將會學到一個優雅退出的方案。

程式崩潰優雅退出

關於錯誤捕獲,Node.js 官網曾提供了一個模組 domain 來實現,但是現在已廢棄了所以就不再考慮了。

之前在看 這個專案時看到了以下關於錯誤退出的一段程式碼:

// 
graceful({
  server: [registry, web],
  error: function (err, throwErrorCount) {
    if (err.message) {
      err.message += ' (uncaughtException throw ' + throwErrorCount + ' times on pid:' + process.pid + ')';
    }
    console.error(err);
    console.error(err.stack);
    logger.error(err);
  }
});

上述使用的是 這個模組,在 NPM 上可以找到。

實現一個 graceful.js

實現一個 graceful 函式,初始化載入時註冊 uncaughtException、unhandledRejection 兩個錯誤事件,分別監聽未捕獲的錯誤資訊和未捕獲的 Promise 錯誤資訊。

const http = require('http');

/**
 * graceful
 * @param { Number } options.killTimeout 超時時間
 * @param { Function } options.onError 產生錯誤資訊會執行該回撥函式
 * @param { Array } options.servers Http Server
 * @returns
 */
function graceful(options = {}) {
  options.killTimeout = options.killTimeout || 1000 * 30;
  options.onError = options.onError || function () {};
  options.servers= options.servers || [];
  process.on('uncaughtException', error => handleUncaughtException(error, options));
  process.on('unhandledRejection', error => handleUnhandledRejection(error, options));
}

handleUncaughtException、handleUnhandledRejection 分別接收相應的錯誤事件,執行應用傳入的 onError() 將錯誤資訊進行回傳,最後呼叫 handleError()。

const throwCount = {
  uncaughtException: 0,
  unhandledRejection: 0
};
function handleUncaughtException(error, options) {
  throwCount.uncaughtException += 1;
  options.onError(error, 'uncaughtException', throwCount.uncaughtException);

  if (throwCount.uncaughtException > 1) return;
  handleError(options);
};

function handleUnhandledRejection(error, options) {
  throwCount.unhandledRejection += 1;
  options.onError(error, 'unhandledRejection', throwCount.unhandledRejection);

  if (throwCount.unhandledRejection > 1) return;
  handleError(options);
}

HandleError 方法為核心實現,首先遍歷應用傳入的 servers,監聽 request 事件,在未捕獲錯誤觸發之後,如果還有請求連結,則關閉當前請求的連結。

之後,執行 setTimeout 延遲退出,也就是最大可能的等待之前連結處理完成。

function handleError(options) {
  const { servers, killTimeout } = options;
  // 關閉當前請求的連結
  for (const server of servers) {
    console.log('server instanceof http.Server: ', server instanceof http.Server);
    if (server instanceof http.Server) {
      server.on('request', (req, res) => {
        req.shouldKeepAlive = false;
        res.shouldKeepAlive = false;
        if (!res._header) {
          res.setHeader('Connection', 'close');
        }
      });
    }
  }

  // 延遲退出
  const timer = setTimeout(() => {
    process.exit(1);
  }, killTimeout);
  
  if (timer && timer.unref) {
    timer.unref();
  }
}
module.exports = graceful;

應用程式中使用上述實現

載入上述 graceful.js 使用起來很簡單隻需要在檔案尾部,載入 graceful 函式並傳入相應引數即可。

const graceful = require('./graceful.js');
...
server.listen(PORT, () => console.log(`port is listening on ${PORT}.`));

graceful({
  servers: [server],
  onError: (error, type, throwErrorCount) => {
    console.log('[%s] [pid: %s] [throwErrorCount: %s] %s: %s', new Date(), process.pid, throwErrorCount, type, error.stack || error);
  }
});

再次執行應用程式,看看效果:

圖片描述

這一次,即使右側 /error 路由產生未捕獲異常,也將不會引起左側請求無法正常響應。

Graceful 模組

最後推薦一個 NPM 模組 ,引用文件中的一句話:“It’s the best way to handle uncaughtException on current situations.”

該模組還提供了對於 Node.js 中 Cluster 模組的支援。

安裝

$ npm install graceful -S

應用

如果一個程式中有多個 Server,將它們新增到 servers 中即可。

const graceful = require('graceful');
...

graceful({
  servers: [server1, server2, restapi],
  killTimeout: '15s',
});

總結

如果你正在使用 Node.js 對於異常你需要有些瞭解,上述講解的兩個異常事件可以做為你的最後補救措施,但是不應該當作 On Error Resume Next(出了錯誤就恢復讓它繼續)的等價機制。

如果你有不錯的建議歡迎和我一起討論!

Reference

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/756/viewspace-2825710/,如需轉載,請註明出處,否則將追究法律責任。

相關文章