在 Node.js 中,提供了 error 模組,並且內建了標準的 JavaScript 錯誤,常見的有:
本系列所有的範例原始碼都已上傳至Github,點選此處獲取。
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 種常用的捕獲方法:
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: {} }
參考資料: