大多數工程師可能并沒留意過 JS 中錯誤對象、錯誤堆棧的細節,即使他們每天的日常工作會面臨不少的報錯,部分同學甚至在 console 的錯誤面前一臉懵逼,不知道從何開始排查,如果你對本文講解的内容有系統的了解,就會從容很多。而錯誤堆棧清理能讓你有效去掉噪音資訊,聚焦在真正重要的地方,此外,如果了解了 Error 的各種屬性到底是什麼,你就能更好的利用他。
接下來,我們就直奔主題。

調用棧的工作機制
在探讨 JS 中的錯誤之前,我們必須了解調用棧(Call Stack)的工作機制,其實這個機制非常簡單,如果你對這個已經一清二楚了,可以直接跳過這部分内容。
簡單的說:函數被調用時,就會被加入到調用棧頂部,執行結束之後,就會從調用棧頂部移除該函數,這種資料結構的關鍵在于後進先出,即大家所熟知的 LIFO。比如,當我們在函數 y 内部調用函數 x 的時候,調用棧從下往上的順序就是 y -> x 。
我們再舉個代碼執行個體:
function c() { console.log('c');}function b() { console.log('b'); c();}function a() { console.log('a'); b();}a();
這段代碼運作時,首先 a 會被加入到調用棧的頂部,然後,因為 a 内部調用了 b,緊接着 b 被加入到調用棧的頂部,當 b 内部調用 c 的時候也是類似的。在調用 c的時候,我們的調用棧從下往上會是這樣的順序:a -> b -> c。在 c 執行完畢之後,c 被從調用棧中移除,控制流回到 b 上,調用棧會變成:a -> b,然後 b 執行完之後,調用棧會變成:a,當 a 執行完,也會被從調用棧移除。
為了更好的說明調用棧的工作機制,我們對上面的代碼稍作改動,使用 console.trace 來把目前的調用棧輸出到 console 中,你可以認為console.trace 列印出來的調用棧的每一行出現的原因是它下面的那行調用而引起的。
function c() { console.log('c'); console.trace();}function b() { console.log('b'); c();}function a() { console.log('a'); b();}a();
當我們在 Node.js 的 REPL 中運作這段代碼,會得到如下的結果:
Trace at c (repl:3:9) at b (repl:3:1) at a (repl:3:1) at repl:1:1 //
顯而易見,當我們在 c 内部調用 console.trace 的時候,調用棧從下往上的結構是:a -> b -> c。如果把代碼再稍作改動,在 b 中 c 執行完之後調用,如下:
function c() { console.log('c');}function b() { console.log('b'); c(); console.trace();}function a() { console.log('a'); b();}a();
通過輸出結果可以看到,此時列印的調用棧從下往上是:a -> b,已經沒有 c 了,因為 c 執行完之後就從調用棧移除了。
Trace at b (repl:4:9) at a (repl:3:1) at repl:1:1 //
再總結下調用棧的工作機制:調用函數的時候,會被推到調用棧的頂部,而執行完畢之後,就會從調用棧移除。
call stack是什麼錯誤_你不知道的 JavaScript 錯誤和調用棧常識
Error 對象及錯誤處理
當代碼中發生錯誤時,我們通常會抛出一個 Error 對象。Error 對象可以作為擴充和建立自定義錯誤類型的原型。Error 對象的 prototype 具有以下屬性:
- constructor – 負責該執行個體的原型構造函數;
- message – 錯誤資訊;
- name – 錯誤的名字;
上面都是标準屬性,有些 JS 運作環境還提供了标準屬性之外的屬性,如 Node.js、Firefox、Chrome、Edge、IE 10、Opera 和 Safari 6+ 中會有 stack 屬性,它包含了錯誤代碼的調用棧,接下來我們簡稱錯誤堆棧。錯誤堆棧包含了産生該錯誤時完整的調用棧資訊。如果您想了解更多關于 Error 對象的非标準屬性,我強烈建議你閱讀 MDN 的這篇文章。
抛出錯誤時,你必須使用 throw 關鍵字。為了捕獲抛出的錯誤,則必須使用 try catch 語句把可能出錯的代碼塊包起來,catch 的時候可以接收一個參數,該參數就是被抛出的錯誤。與 Java 中類似,JS 中也可以在 try catch 語句之後有 finally,不論前面代碼是否抛出錯誤 finally 裡面的代碼都會執行,這種語言的常見用途有:在 finally 中做些清理的工作。
此外,你可以使用沒有 catch 的 try 語句,但是後面必須跟上 finally,這意味着我們可以使用三種不同形式的 try 語句:
- try … catch
- try … finally
- try … catch … finally
try 語句還可以嵌套在 try 語句中,比如:
try { try { throw new Error('Nested error.'); // 這裡的錯誤會被自己緊接着的 catch 捕獲 } catch (nestedErr) { console.log('Nested catch'); // 這裡會運作 }} catch (err) { console.log('This will not run.'); // 這裡不會運作}
try 語句也可以嵌套在 catch 和 finally 語句中,比如下面的兩個例子:
try { throw new Error('First error');} catch (err) { console.log('First catch running'); try { throw new Error('Second error'); } catch (nestedErr) { console.log('Second catch running.'); }}try { console.log('The try block is running...');} finally { try { throw new Error('Error inside finally.'); } catch (err) { console.log('Caught an error inside the finally block.'); }}
同樣需要注意的是,你可以抛出不是 Error 對象的任意值。這可能看起來很酷,但在工程上卻是強烈不建議的做法。如果恰巧你需要處理錯誤的調用棧資訊和其他有意義的中繼資料,抛出非 Error 對象的錯誤會讓你的處境很尴尬。
假如我們有如下的代碼:
function runWithoutThrowing(func) { try { func(); } catch (e) { console.log('There was an error, but I will not throw it.'); console.log('The error's message was: ' + e.message) }}function funcThatThrowsError() { throw new TypeError('I am a TypeError.');}runWithoutThrowing(funcThatThrowsError);
如果 runWithoutThrowing 的調用者傳入的函數都能抛出 Error 對象,這段代碼不會有任何問題,如果他們抛出了字元串那就有問題了,比如:
function runWithoutThrowing(func) { try { func(); } catch (e) { console.log('There was an error, but I will not throw it.'); console.log('The error's message was: ' + e.message) }}function funcThatThrowsString() { throw 'I am a String.';}runWithoutThrowing(funcThatThrowsString);
這段代碼運作時,runWithoutThrowing 中的第 2 次 console.log 會抛出錯誤,因為 e.message 是未定義的。這些看起來似乎沒什麼大不了的,但如果你的代碼需要使用 Error 對象的某些特定屬性,那麼你就需要做很多額外的工作來確定一切正常。如果你抛出的值不是 Error 對象,你就不會拿到錯誤相關的重要資訊,比如 stack,雖然這個屬性在部分 JS 運作環境中才會有。
Error 對象也可以向其他對象那樣使用,你可以不用抛出錯誤,而隻是把錯誤傳遞出去,Node.js 中的錯誤優先回調就是這種做法的典型範例,比如 Node.js 中的 fs.readdir 函數:
const fs = require('fs');fs.readdir('/example/i-do-not-exist', function callback(err, dirs) { if (err) { // `readdir` will throw an error because that directory does not exist // We will now be able to use the error object passed by it in our callback function console.log('Error Message: ' + err.message); console.log('See? We can use Errors without using try statements.'); } else { console.log(dirs); }});
此外,Error 對象還可以用于 Promise.reject 的時候,這樣可以更容易的處理 Promise 失敗,比如下面的例子:
new Promise(function(resolve, reject) { reject(new Error('The promise was rejected.'));}).then(function() { console.log('I am an error.');}).catch(function(err) { if (err instanceof Error) { console.log('The promise was rejected with an error.'); console.log('Error Message: ' + err.message); }});
錯誤堆棧的裁剪
Node.js 才支援這個特性,通過 Error.captureStackTrace 來實作,Error.captureStackTrace 接收一個 object 作為第 1 個參數,以及可選的 function 作為第 2 個參數。其作用是捕獲目前的調用棧并對其進行裁剪,捕獲到的調用棧會記錄在第 1 個參數的 stack 屬性上,裁剪的參照點是第 2 個參數,也就是說,此函數之前的調用會被記錄到調用棧上面,而之後的不會。
讓我們用代碼來說明,首先,把目前的調用棧捕獲并放到 myObj 上:
const myObj = {};function c() {}function b() { // 把目前調用棧寫到 myObj 上 Error.captureStackTrace(myObj); c();}function a() { b();}// 調用函數 aa();// 列印 myObj.stackconsole.log(myObj.stack);// 輸出會是這樣// at b (repl:3:7)
上面的調用棧中隻有 a -> b,因為我們在 b 調用 c 之前就捕獲了調用棧。現在對上面的代碼稍作修改,然後看看會發生什麼:
const myObj = {};function d() { // 我們把目前調用棧存儲到 myObj 上,但是會去掉 b 和 b 之後的部分 Error.captureStackTrace(myObj, b);}function c() { d();}function b() { c();}function a() { b();}// 執行代碼a();// 列印 myObj.stackconsole.log(myObj.stack);// 輸出如下// at a (repl:2:1)
在這段代碼裡面,因為我們在調用 Error.captureStackTrace 的時候傳入了 b,這樣 b 之後的調用棧都會被隐藏。
現在你可能會問,知道這些到底有啥用?如果你想對使用者隐藏跟他業務無關的錯誤堆棧(比如某個庫的内部實作)就可以試用這個技巧。
總結
通過本文的描述,相信你對 JS 中的調用棧、Error 對象、錯誤堆棧有了清晰的認識,在遇到錯誤的時候不在慌亂。如果對文中的内容有任何疑問,歡迎在下面評論。
call stack是什麼錯誤_你不知道的 JavaScript 錯誤和調用棧常識
如果您對這個文章有任何異議,那麼請在文章評論處寫上你的評論。
如果您覺得這個文章有意思,那麼請分享并轉發,或者也可以關注一下表示您對我們文章的認可與鼓勵。
願大家都能在程式設計這條路,越走越遠。