NodeJS 提供了 domain 模組,可以簡化非同步程式碼的異常處理。在介紹這個模組之前,我們需要先理解「域」的概念。簡單的講,一個域就是一個 JS 運行環境,在一個運行環境中,如果一個異常沒有被捕獲,將作為一個全域異常被拋出。 NodeJS 透過 process 物件提供了捕捉全域異常的方法,範例程式碼如下
process.on('uncaughtException', function (err) { console.log('Error: %s', err.message); }); setTimeout(function (fn) { fn(); });
Error: undefined is not a function
雖然全域異常有個地方可以捕獲了,但是對於大多數異常,我們希望儘早捕獲,並根據結果決定程式碼的執行路徑。我們用以下 HTTP 伺服器程式碼作為範例:
function async(request, callback) { // Do something. asyncA(request, function (err, data) { if (err) { callback(err); } else { // Do something asyncB(request, function (err, data) { if (err) { callback(err); } else { // Do something asyncC(request, function (err, data) { if (err) { callback(err); } else { // Do something callback(null, data); } }); } }); } }); } http.createServer(function (request, response) { async(request, function (err, data) { if (err) { response.writeHead(500); response.end(); } else { response.writeHead(200); response.end(data); } }); });
以上程式碼將請求物件交給非同步函數處理後,再根據處理結果回傳回應。這裡採用了使用回呼函數傳遞異常的方案,因此 async 函數內部如果再多幾個非同步函數呼叫的話,程式碼就變成上邊這副鬼樣子了。為了讓程式碼好好看點,我們可以在每處理一個請求時,使用 domain 模組建立一個子域(JS 子運行環境)。在子域內運行的程式碼可以隨意拋出異常,而這些異常可以透過子域物件的 error 事件統一捕獲。於是以上程式碼可以做以下改造:
function async(request, callback) { // Do something. asyncA(request, function (data) { // Do something asyncB(request, function (data) { // Do something asyncC(request, function (data) { // Do something callback(data); }); }); }); } http.createServer(function (request, response) { var d = domain.create(); d.on('error', function () { response.writeHead(500); response.end(); }); d.run(function () { async(request, function (data) { response.writeHead(200); response.end(data); }); }); });
可以看到,我們使用.create方法建立了一個子域對象,並透過.run方法進入需要在子域中運行的程式碼的入口點。而位於子域中的非同步函數回呼函數由於不再需要捕獲異常,程式碼一下子瘦身很多。
陷阱
無論是透過process 物件的uncaughtException 事件擷取到全域異常,還是透過子網域物件的error 事件擷取到了子網域異常,在NodeJS 官方文件裡都強烈建議處理完異常後立即重啟程序,而不是讓程式繼續運行。根據官方文件的說法,發生異常後的程式處於不確定的運行狀態,如果不立即退出的話,程式可能會發生嚴重記憶體洩漏,也可能表現得很奇怪。
但這裡需要澄清一些事實。 JS 本身的throw..try..catch異常處理機制並不會導致記憶體洩漏,也不會讓程式的執行結果出乎意料,但 NodeJS 並不是存粹的 JS。 NodeJS 裡大量的API 內部是用C/C++ 實現的,因此NodeJS 程式的運行過程中,程式碼執行路徑穿梭於JS 引擎內部和外部,而JS 的異常拋出機制可能會打斷正常的程式碼執行流程,導致C/C++ 部分的程式碼表現異常,進而導致記憶體洩漏等問題。
因此,使用 uncaughtException 或 domain 捕獲異常,程式碼執行路徑裡涉及到了 C/C++ 部分的程式碼時,如果無法確定是否會導致記憶體洩漏等問題,最好在處理完異常後重啟程式比較妥當。而使用 try 語句捕捉異常時一般捕獲到的都是 JS 本身的異常,不用擔心上訴問題。