天天看點

JavaScript錯誤處理和堆棧追蹤淺析

JavaScript錯誤處理和堆棧追蹤淺析

有時我們會忽略錯誤處理和堆棧追蹤的一些細節, 但是這些細節對于寫與測試或錯誤處理相關的庫來說是非常有用的. 例如這周, 對于 chai

就有一個非常棒的pr, 該pr極大地改善了我們處理堆棧的方式, 當使用者的斷言失敗的時候, 我們會給予更多的提示資訊(幫助使用者進行定位).

合理地處理堆棧資訊能使你清除無用的資料, 而隻專注于有用的資料. 同時, 當更好地了解 errors 對象及其相關屬性之後, 能有助于你更充分地利用 errors.

(函數的)調用棧是怎麼工作的

在談論錯誤之前, 先要了解下(函數的)調用棧的原理:

當有一個函數被調用的時候, 它就被壓入到堆棧的頂部, 該函數運作完成之後, 又會從堆棧的頂部被移除.

堆棧的資料結構就是後進先出, 以 lifo (last in, first out) 著稱.

例如:

function c() { 

    console.log('c'); 

function b() { 

    console.log('b'); 

    c(); 

function a() { 

    console.log('a'); 

    b(); 

a();  

在上述的示例中, 當函數 a 運作時, 其會被添加到堆棧的頂部. 然後, 當函數 b 在函數 a 的内部被調用時, 函數 b 會被壓入到堆棧的頂部. 當函數 c 在函數 b 的内部被調用時也會被壓入到堆棧的頂部.

當函數 c 運作時, 堆棧中就包含了 a, b 和 c(按此順序).

當函數 c 運作完畢之後, 就會從堆棧的頂部被移除, 然後函數調用的控制流就回到函數 b. 函數 b 運作完之後, 也會從堆棧的頂部被移除, 然後函數調用的控制流就回到函數 a. 最後, 函數 a 運作完成之後也會從堆棧的頂部被移除.

為了更好地在demo中示範堆棧的行為, 可以使用 console.trace() 在控制台輸出目前的堆棧資料. 同時, 你要以從上至下的順序閱讀輸出的堆棧資料.

    console.trace(); 

在 node 的 repl 模式中運作上述代碼會得到如下輸出:

trace 

    at c (repl:3:9) 

    at b (repl:3:1) 

    at a (repl:3:1) 

    at repl:1:1 // <-- for now feel free to ignore anything below this point, these are node's internals 

    at realruninthiscontextscript (vm.js:22:35) 

    at siginthandlerswrap (vm.js:98:12) 

    at contextifyscript.script.runinthiscontext (vm.js:24:12) 

    at replserver.defaulteval (repl.js:313:29) 

    at bound (domain.js:280:14) 

    at replserver.runbound [as eval] (domain.js:293:12)  

正如所看到的, 當從函數 c 中輸出時, 堆棧中包含了函數 a, b 以及c.

如果在函數 c 運作完成之後, 在函數 b 中輸出目前的堆棧資料, 就會看到函數 c 已經從堆棧的頂部被移除, 此時堆棧中僅包括函數 a 和 b.

}  

正如所看到的, 函數 c 運作完成之後, 已經從堆棧的頂部被移除.

    at b (repl:4:9) 

    at repl:1:1  // <-- for now feel free to ignore anything below this point, these are node's internals 

    at replserver.runbound [as eval] (domain.js:293:12) 

    at replserver.online (repl.js:513:10)  

error對象和錯誤處理

當程式運作出現錯誤時, 通常會抛出一個 error 對象. error 對象可以作為使用者自定義錯誤對象繼承的原型.

error.prototype 對象包含如下屬性:

constructor–指向執行個體的構造函數

message–錯誤資訊

name–錯誤的名字(類型)

上述是 error.prototype 的标準屬性, 此外, 不同的運作環境都有其特定的屬性. 在例如 node, firefox,

chrome, edge, ie 10+, opera 以及 safari 6+ 這樣的環境中, error 對象具備 stack 屬性,

該屬性包含了錯誤的堆棧軌迹. 一個錯誤執行個體的堆棧軌迹包含了自構造函數之後的所有堆棧結構.

如果想了解更多關于 error 對象的特定屬性, 可以閱讀 mdn 上的這篇文章.

為了抛出一個錯誤, 必須使用 throw 關鍵字. 為了 catch 一個抛出的錯誤, 必須使用 try...catch 包含可能跑出錯誤的代碼. catch的參數是被跑出的錯誤執行個體.

如 java 一樣, javascript 也允許在 try/catch 之後使用 finally 關鍵字. 在處理完錯誤之後, 可以在finally 語句塊作一些清除工作.

在文法上, 你可以使用 try 語句塊而其後不必跟着 catch 語句塊, 但必須跟着 finally 語句塊. 這意味着有三種不同的 try 語句形式:

try...catch

try...finally

try...catch...finally

try語句内還可以在嵌入 try 語句:

try { 

    try { 

        throw new error('nested error.'); // the error thrown here will be caught by its own `catch` clause 

    } catch (nestederr) { 

        console.log('nested catch'); // this runs 

    } 

} catch (err) { 

    console.log('this will not run.'); 

也可以在 catch 或 finally 中嵌入 try 語句:

    console.log('the try block is running...'); 

} finally { 

        throw new error('error inside finally.'); 

    } catch (err) { 

        console.log('caught an error inside the finally block.'); 

需要重點說明一下的是在抛出錯誤時, 可以隻抛出一個簡單值而不是 error 對象. 盡管這看起來看酷并且是允許的,

但這并不是一個推薦的做法, 尤其是對于一些需要處理他人代碼的庫和架構的開發者, 因為沒有标準可以參考, 也無法得知會從使用者那裡得到什麼.

你不能信任使用者會抛出 error 對象, 因為他們可能不會這麼做, 而是簡單的抛出一個字元串或者數值.

這也意味着很難去處理堆棧資訊和其它元資訊.

function runwithoutthrowing(func) { 

        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 的參數抛出了一個錯誤對象, 上面的代碼能正常捕獲錯誤. 然後, 如果是抛出一個字元串, 就會碰到一些問題了:

function functhatthrowsstring() { 

    throw 'i am a string.'; 

runwithoutthrowing(functhatthrowsstring);  

現在第二個 console.log 會輸出undefined. 這看起來不是很重要, 但如果你需要確定 error

對象有一個特定的屬性或者用另一種方式來處理 error 對象的特定屬性(例如 chai的throws斷言的做法),

你就得做大量的工作來確定程式的正确運作.

同時, 如果抛出的不是 error 對象, 也就擷取不到 stack 屬性.

errors 也可以被作為其它對象, 你也不必抛出它們, 這也是為什麼大多數回調函數把 errors 作為第一個參數的原因. 例如:

const fs = require('fs'); 

fs.readdir('/example/i-do-not-exist', function callback(err, dirs) { 

    if (err instanceof error) { 

        // `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 對象也可以用于 rejected promise, 這使得很容易處理 rejected promise:

new promise(function(resolve, reject) { 

    reject(new error('the promise was rejected.')); 

}).then(function() { 

    console.log('i am an error.'); 

}).catch(function(err) { 

        console.log('the promise was rejected with an error.'); 

處理堆棧

這一節是針對支援 error.capturestacktrace的運作環境, 例如nodejs.

error.capturestacktrace 的第一個參數是 object, 第二個可選參數是一個

function.error.capturestacktrace 會捕獲堆棧資訊, 并在第一個參數中建立 stack

屬性來存儲捕獲到的堆棧資訊. 如果提供了第二個參數, 該函數将作為堆棧調用的終點. 是以, 捕獲到的堆棧資訊将隻顯示該函數調用之前的資訊.

用下面的兩個demo來解釋一下. 第一個, 僅将捕獲到的堆棧資訊存于一個普通的對象之中:

const myobj = {}; 

    // here we will store the current stack trace into myobj 

    error.capturestacktrace(myobj); 

// first we will call these functions 

a(); 

// now let's see what is the stack trace stored into myobj.stack 

console.log(myobj.stack); 

// this will print the following stack to the console: 

//    at b (repl:3:7) <-- since it was called inside b, the b call is the last entry in the stack 

//    at a (repl:2:1) 

//    at repl:1:1 <-- node internals below this line 

//    at realruninthiscontextscript (vm.js:22:35) 

//    at siginthandlerswrap (vm.js:98:12) 

//    at contextifyscript.script.runinthiscontext (vm.js:24:12) 

//    at replserver.defaulteval (repl.js:313:29) 

//    at bound (domain.js:280:14) 

//    at replserver.runbound [as eval] (domain.js:293:12) 

//    at replserver.online (repl.js:513:10)  

從上面的示例可以看出, 首先調用函數 a(被壓入堆棧), 然後在 a 裡面調用函數 b(被壓入堆棧且在a之上), 然後在 b 中捕獲到目前的堆棧資訊, 并将其存儲到 myobj 中. 是以, 在控制台輸出的堆棧資訊中僅包含了 a和 b 的調用資訊.

現在, 我們給 error.capturestacktrace 傳遞一個函數作為第二個參數, 看下輸出資訊:

function d() { 

    // this time we will hide all the frames after `b` and `b` itself 

    error.capturestacktrace(myobj, b); 

    d(); 

//    at a (repl:2:1) <-- as you can see here we only get frames before `b` was called 

//    at replserver.online (repl.js:513:10) 

//    at emitone (events.js:101:20)  

當将函數 b 作為第二個參數傳給 error.capturestacktracefunction 時, 輸出的堆棧就隻包含了函數 b

調用之前的資訊(盡管 error.capturestacktracefunction 是在函數 d 中調用的),

這也就是為什麼隻在控制台輸出了 a. 這樣處理方式的好處就是用來隐藏一些與使用者無關的内部實作細節.

作者:佚名

來源:51cto