Node 錯誤處理之挖坑系列

一. Error

    JS 中的 Error 對象. 包含了錯誤的具體信息,包括 name、message、錯誤堆棧 stack 等。能夠以 new Error 方式建立實例拋出,或調用 Error.captureStackTrace 爲已有對象添加 stack 錯誤堆棧信息 然後拋出
clipboard.pngjavascript

二. 錯誤拋出幾種方式

  • Throw
    Javascript 拋出的異常,是以 throw 方法拋出,未必都是 Error 的實例,但經過 nodeJs 或者 js 運行時發生的錯誤,都是 Error 的實例
  • EventEmitter
    Nodejs 形式的錯誤回調,大部分流 & 異步事件都衍生自 EventEmitter 類 || 實例,如 fs, process, stream 等
  • Process
    程序運行過程當中拋出的異常,或由底層庫拋出,或是運行中發生的一些 SyntaxError 之類

三. 錯誤捕獲幾種方式

try .. catchhtml

經常使用的一種捕獲錯誤方式,瀏覽器 || node 環境均適用java

缺點:只針對同步異常有效node

try {
        throw new Error('Something went wrong here')
    } catch (err) {
        console.log(err)
    }

EventEmitter api

    由 Events 模塊提供的 EventEmitter 類,基於 Observer 模式作的 publish/subscribe,經過 .on('error', ...) || .addEventlistener('error', ...) 註冊 subscriber,.emit() 發佈事件,但會有最大的 maxListener 的限制,可更改。
    不 show 源碼了,特別簡單,本身去 look 一下。如 koa 的 app 就是基於 EventEmitter 的擴展,所以能夠經過監聽 errorpromise

class Koa extends EventEmitter {...}
    
    let app = new Koa()
    app.emit('error', ..)
    app.on('error', ...)

Process 瀏覽器

Process 進程對象也是 EventEmitter 的實例,可經過以下兩事件監聽 errorapp

  • unhandledRejection
        promise 的回調報錯,可經過監聽該事件 catch,但要注意因爲 promise 的 rejection catched 不知道會在啥時候才發生,因此實際上可能在 unhandledRejection 事件觸發後才 catch 了這個信息,對應有 rejectionHandled 事件監聽,以下:
process.on('rejectionHandled', p => {
        console.log('It has been handled')
    })
    
    process.on('unhandledRejection', (reason, p) => {
        console.log('Went here')
    })
    
    let test = new Promise((resolve, reject) => {
        let err = new Error('Just for a test')
        err.name = 'TestError'
        reject(err)
    })
    
    setTimeout(() => {
        test.catch(err => {
            console.log('Excample work!')
            console.log(err)
        })
    }, 10000)

    // 打印出來的信息順序是:
    // Went here
    // It has been handled
    // Excample work!
    // { TestError: Just for a test
    // ....(stack message here) }

(承上)
uncaughtException
    其他 js 運行中發生 || 拋出的未捕獲錯誤,都可經過監聽該事件解決,若不進行該事件的監聽,發生異常時,會直接致使程序 crash
    但不建議用這種方式 catch,程序運行中的錯誤 更應該是拋出來,全部的 error 都 catch 的話,豈不是程序均可以當作無 bug 了
    but 在打錯誤日誌的時候是須要 catch 上報日誌的,可是在上報完後,須要繼續把 error throw,對於 uncaughtException callback 中拋出的異常不會再捕獲,而是以非 0 的狀態碼 exitdom

// Promise Rejection
    process.on('unhandledRejection', err => {
        process.nextTick(() => { throw err }))
    })
    // 終極 boss
    process.on('uncaughtException', err => {...})
};

四.小結

以上以一張圖爲總結:clipboard.pngkoa

五. 源碼解讀

補充以前沒補充完的內容,下圖爲 node 中對於 process 的初始化等系列流程

  • node.cc 實際上是 node 運行主要的文件,其中定義了三個重載函數 Start,調用順序爲 3 → 2 → 1,每一個函數參數不一樣處理不一樣的邏輯;
  • isolate->AddMessageListener 的監聽事件 OnMessage 會在 js 運行發生錯誤時觸發,嗯,是的,只有 error 纔會傳遞 ;這很好的解釋了爲何 process 監聽 uncaughtException 就能夠監聽到全部的拋出的非 promise 異常;
  • OnMessage 調用了 FatalException 函數,FatalException 引用 process._fatalException 並傳入 error (env 是 env.h 中聲明的 Environment 類實例,fatal_exception_string 也是其中定義的,返回值爲 '_fatalException');
    如下爲 FatalException 函數的部份內容
void FatalException(Isolate* isolate,
                    Local<Value> error,
                    Local<Message> message) {
...(省略)
  Local<String> fatal_exception_string = env->fatal_exception_string(); // '_fatalException'
  Local<Function> fatal_exception_function =
      process_object->Get(fatal_exception_string).As<Function>();
...(省略)
if (exit_code == 0) {
    TryCatch fatal_try_catch(isolate); // 調用 v8::TryCatch 
    
    // Do not call FatalException when _fatalException handler throws
    fatal_try_catch.SetVerbose(false); // 關鍵點

    // this will return true if the JS layer handled it, false otherwise
    Local<Value> caught =
        fatal_exception_function->Call(process_object, 1, &error); // 運行 process._fatalException 函數

    if (fatal_try_catch.HasCaught()) { // 捕獲錯誤
      // the fatal exception function threw, so we must exit
      ReportException(env, fatal_try_catch);
      exit_code = 7;
    }

    if (exit_code == 0 && false == caught->BooleanValue()) {
      ReportException(env, error, message);
      exit_code = 1;
    }
  }
...(省略)
}
  • 在調用 _fatalException 函數前,先調用 v8::TryCatch::setVerbose 把 verbose 設置爲 false,則運行時拋出的異常就不會再觸發 FatalException ;在捕獲到運行錯誤後,把 exit_code 設爲 7,並返回;這就解釋了 爲何在 uncaughtException 時拋出的異常不會再從新觸發回調,要知道 EventEmitter 可沒幫你作這樣的事情
  • _fatalException 在觸發 "uncaughtException" 事件前實際上是會優先檢查 domain 是否存在,當 domain 不存在時纔會調用 uncaughtException 的,但 domain 這個 api 已經被廢除了,也就不累述了
  • unhandledRejection 事件的觸發總體思路也差很少,首先會調用 SetupPromises 初始化,而後調用 v8::Isolate::SetPromiseRejectCallback 進行監聽 ... 而且跟 uncaughtException 不一樣的是 unhandledRejection 的觸發只會打印 warning 並不會把整個程序給 crash 了

六. END

若本文有誤,歡迎隨時指正

相關文章
相關標籤/搜索