Node.js精進(8)——錯誤處理

2022-07-06 12:06:42

  在 Node.js 中,提供了 error 模組,並且內建了標準的 JavaScript 錯誤,常見的有:

  • EvalError:在呼叫 eval() 函數時出現問題時丟擲該錯誤。
  • SyntaxError:呼叫不符合 JavaScript 的語法時丟擲該錯誤。
  • RangeError:超出可接受值的集合或範圍,例如陣列越界。
  • ReferenceError:存取未定義的變數時丟擲該錯誤。
  • TypeError:引數或變數的型別有問題時丟擲該錯誤。
  • URIError:使用全域性的 URI 處理常式發生問題時丟擲該錯誤。

  本系列所有的範例原始碼都已上傳至Github,點選此處獲取。 

一、Error 類

  Node.js 生成的上述錯誤,都是 Error 類的範例或繼承自 Error 類。注意,執行時丟擲的所有異常都將是 Error 的範例。

  Error 範例能捕獲堆疊跟蹤,並提供錯誤的文字描述。

  下面是一個簡單的範例,其中 message 屬性提供了錯誤的字串描述,toString() 會生成文字訊息。

  stack 屬性提供了完整的錯誤資訊,包括錯誤描述和一系列堆疊幀(每行以 "at " 開頭),每一幀都描述了程式碼中生成錯誤的呼叫點。

const e = new Error('test error');
// test error
console.log(e.message);
// Error: test error
console.log(e.toString());
// Error: test error
//     at Object.<anonymous> (/Users/code/web/node/08/error.js:1:11)
//     at Module._compile (node:internal/modules/cjs/loader:1108:14)
//     at Object.Module._extensions..js (node:internal/modules/cjs/loader:1137:10)
//     at Module.load (node:internal/modules/cjs/loader:988:32)
//     at Function.Module._load (node:internal/modules/cjs/loader:828:14)
//     at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:76:12)
//     at node:internal/main/run_main_module:17:47
console.log(e.stack);

二、捕獲錯誤

  一些異常在 JavaScript 層是不可恢復的,會導致 Node.js 程序崩潰。

  所以有些異常需要被捕獲,在 Node.js 中有 3 種常用的捕獲方法:

  • 錯誤優先的回撥。
  • throw 語句或 try-catch 語句。
  • error 事件機制。

1)錯誤優先的回撥

  Node.js 核心模組暴露的大多數非同步方法都遵循錯誤優先回撥的慣用模式。

  使用這種模式,回撥函數作為引數傳給方法,當操作完成或出現錯誤時,回撥函數將使用 Error 範例作為第一個引數傳入。

  如果沒有出現錯誤,則第一個引數將作為 null 傳入。在下面的範例中,當讀取一個不存在的檔案時,將丟擲錯誤。

const fs = require('fs');
function errorCallback(err, data) {
// [Error: ENOENT: no such file or directory, open './data.txt'] {
//   errno: -2,
//   code: 'ENOENT',
//   syscall: 'open',
//   path: './data.txt'
// }
  console.log(err);
}
fs.readFile('./data.txt', errorCallback);

2)throw

  throw 關鍵字後面可以跟任何型別的 JavaScript 值(字串、數位或物件等)。

  不過在 Node.js 中,throw 不會丟擲字串,而僅丟擲 Error 範例。

  直接丟擲 Error 範例,和丟擲其他型別的值,前者會顯示堆疊幀,而後者不會,如下所示。

// /Users/code/web/node/08/throw.js:2
//   throw new Error('test error');
//   ^
// Error: test error
//     at test (/Users/code/web/node/08/throw.js:2:9)
//     at Object.<anonymous> (/Users/code/web/node/08/throw.js:7:1)
//     at Module._compile (node:internal/modules/cjs/loader:1108:14)
//     at Object.Module._extensions..js (node:internal/modules/cjs/loader:1137:10)
//     at Module.load (node:internal/modules/cjs/loader:988:32)
//     at Function.Module._load (node:internal/modules/cjs/loader:828:14)
//     at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:76:12)
//     at node:internal/main/run_main_module:17:47
throw new Error('test error');

// /Users/code/web/node/08/throw.js:2
//   throw 'test error';
//   ^
// test error
// (Use `node --trace-uncaught ...` to show where the exception was thrown)
throw 'test error';

3)try-catch

  try-catch 語句不僅能捕獲同步程式碼,還能捕獲非同步的 async/await 發生的錯誤,如下所示,呼叫一個不存在的 func() 函數。

// 同步程式碼
function try1() {
  try{
    func();
  }catch(e) {
    console.log(e);
    console.log('try-catch end');
  }
}
// async/await
async function test() {
  func();
}
async function try2() {
  try{
    await test();
  }catch(e) {
    console.log(e);
    console.log('async try-catch end');
  }
}

  有一點需要注意,try-catch 無法捕獲非同步的回撥函數,例如定時器、process.nextTick() 中的回撥。

try {
  process.nextTick(function () {
    func();
  });
} catch (e) {
  console.log('nextTick end');
}

  catch 分支中的列印並不會執行,因為當回撥被呼叫時,周圍的程式碼(包括 try-catch)都已經執行好退出了。

4)error 事件機制

  如果在程式執行過程中出現了未捕獲的異常,那麼程式就會崩潰,Node.js 提供了幾個事件來兜底這類未捕獲的異常。

  首先是 process 上的 uncaughtException 事件,當未捕獲的 JavaScript 異常冒泡到事件迴圈時,就會自動觸發該事件。

  就比如上面那個無法捕獲的 try-catch 問題,註冊了 uncaughtException 事件就能成功捕獲,如下所示。

// ReferenceError: func is not defined
//     at /Users/code/web/node/08/uncaughtException.js:7:5
//     at processTicksAndRejections (node:internal/process/task_queues:78:11)
process.on('uncaughtException', (err) => {
  console.log(err);
});

  在捕獲後,就不會讓程式奔潰,後續程式碼也能被順利執行。

  注意,uncaughtException 事件是用於例外處理的粗略機制,僅用作最後的兜底手段,歸根結底,那些異常不能無視還是需要修復的。

  使用 Promise 進行程式設計時,異常被封裝為「被拒絕的 promise」,有兩種捕獲方式。第一種是使用 promise.catch() 捕獲和處理,並通過 Promise 鏈傳播。

  第二種是註冊 process 的 unhandledRejection 事件,當 Promise 被拒絕並且在事件迴圈的一個輪詢內沒有錯誤捕獲時,就會觸發此事件。

  unhandledRejection 事件回撥程式包含兩個引數,第一個是任意型別的 Promise 被拒絕的理由,第二個是被拒絕的 Promise 物件。

process.on('unhandledRejection', (reason, promise) => {
  console.log(reason);
  console.log(promise);
});

  下面是兩種觸發方式,第一種是在 then() 回撥中書寫錯誤程式碼,第二種是繫結 reject() 方法。

// 第一種觸發方式
Promise.resolve().then((res) => {
  return JSON.pasre(res);   // 注意錯別字 pasre
});
// 第二種觸發方式
Promise.reject(new Error('資源尚未載入'));

  unhandledRejection 事件對於檢測和跟蹤尚未處理的被拒絕的 Promise 很有用。

5)verror

  在下面的範例中,會在非同步回撥中通過 throw 丟擲一個錯誤。

function test() {
  throw new Error('test error');
}
function main() {  
  setImmediate(() => test());
}
main();

  注意觀察下面的堆疊資訊,僅僅標註了 test() 函數中出錯的那條語句的位置,但是再往上的 main() 並沒有被標註。

/Users/code/web/node/08/verror.js:2
  throw new Error('test error');
  ^
Error: test error
    at test (/Users/code/web/node/08/verror.js:2:9)
    at Immediate.<anonymous> (/Users/code/web/node/08/verror.js:5:22)
    at processImmediate (node:internal/timers:464:21)

  當函數的呼叫深度比較深時,一旦出錯,那麼追溯程式完整的執行過程就比較困難。

  目前市面上有一款 verror 庫,可以將 Error 範例層層封裝,在每一層附加錯誤資訊,最後通過 VError 範例就能獲取偵錯所需的資訊,便於問題的定位。

const VError = require('verror');
function test(err) {
  const err3 = new VError(err, 'test()');
  console.log(err3.message);  // test(): main(): test error
  console.log(err3);
}
function main() {  
  setImmediate(() => {
    const err1 = new Error('test error');
    const err2 = new VError(err1, 'main()');
    test(err2);
  });
}
main();

  在上面的範例中,先範例化一個 Error 類,然後範例化一個 VError 類,建構函式的第二個引數就是提供給偵錯用的關鍵資訊。

  將 VError 範例作為引數傳遞給 test() 函數,再範例化一個 VError 類,這其實就是層層包裝的過程。

  最後讀取 message 屬性,得到的值是 test(): main(): test error,這些就是附加的資料,以及錯誤描述。

  如果直接列印 VError 範例,那麼能得到更多關鍵資訊,包括行數,檔案路徑等。

VError: test(): main(): test error
    at test (/Users/code/web/node/08/verror.js:3:16)
    at Immediate._onImmediate (/Users/code/web/node/08/verror.js:11:5)
    at processImmediate (node:internal/timers:464:21) {
  jse_shortmsg: 'test()',
  jse_cause: VError: main(): test error
      at Immediate._onImmediate (/Users/code/web/node/08/verror.js:10:18)
      at processImmediate (node:internal/timers:464:21) {
    jse_shortmsg: 'main()',
    jse_cause: Error: test error
        at Immediate._onImmediate (/Users/code/web/node/08/verror.js:9:18)
        at processImmediate (node:internal/timers:464:21),
    jse_info: {}
  },
  jse_info: {}
}

 

 

參考資料:

捕獲異常 診斷報告 

餓了麼偵錯面試題

[譯] NodeJS 錯誤處理最佳實踐

例外處理與domain