node.js - node js try catch




Node.js最佳實踐異常處理 (7)

nodejs域是處理nodejs中錯誤的最新方式。 域可以捕獲錯誤/其他事件以及傳統拋出的對象。 域還提供了處理回調的功能,該回調具有通過攔截方法作為第一個參數傳遞的錯誤。

與正常的try / catch式錯誤處理一樣,通常最好在發生錯誤時拋出錯誤,並且阻止要隔離錯誤的區域影響其餘代碼。 “屏蔽”這些區域的方法是使用一個函數作為一個獨立代碼塊調用domain.run。

在同步代碼中,上面的代碼就足夠了 - 當發生錯誤時,您要么讓它被拋出,要么抓住它並在那里處理,還原任何需要還原的數據。

try {  
  //something
} catch(e) {
  // handle data reversion
  // probably log too
}

當異步回調中發生錯誤時,您需要能夠完全處理數據的回滾(共享狀態,外部數據如數據庫等)。 或者你必須設置一些信息來表明發生了異常 - 你關心那個標誌的地方,你必須等待回調才能完成。

var err = null;
var d = require('domain').create();
d.on('error', function(e) {
  err = e;
  // any additional error handling
}
d.run(function() { Fiber(function() {
  // do stuff
  var future = somethingAsynchronous();
  // more stuff

  future.wait(); // here we care about the error
  if(err != null) {
    // handle data reversion
    // probably log too
  }

})});

以上代碼中的一部分很難看,但您可以為自己創建模式以使其更漂亮,例如:

var specialDomain = specialDomain(function() {
  // do stuff
  var future = somethingAsynchronous();
  // more stuff

  future.wait(); // here we care about the error
  if(specialDomain.error()) {
    // handle data reversion
    // probably log too
  } 
}, function() { // "catch"
  // any additional error handling
});

更新(2013-09):

在上面,我使用了一個暗示纖維語義的未來,它允許您在線等待期貨。 這實際上可以讓你為所有事情使用傳統的try-catch塊 - 我認為這是最好的選擇。 但是,你不能總是這樣做(即在瀏覽器中)...

也有期貨不需要纖維語義(然後與普通的瀏覽JavaScript一起工作)。 這些可以被稱為期貨,承諾或延期(我將從此處引用期貨)。 普通舊式的JavaScript期貨庫允許在期貨之間傳播錯誤。 只有其中一些庫允許任何拋出的未來被正確處理,所以要小心。

一個例子:

returnsAFuture().then(function() {
  console.log('1')
  return doSomething() // also returns a future

}).then(function() {
  console.log('2')
  throw Error("oops an error was thrown")

}).then(function() {
  console.log('3')

}).catch(function(exception) {
  console.log('handler')
  // handle the exception
}).done()

這模仿一個正常的try-catch,即使這些部分是異步的。 它會打印:

1
2
handler

請注意,它不會打印'3',因為拋出的異常會中斷該流。

看看藍鳥的承諾:

請注意,我還沒有發現許多其他庫,這些庫正確處理拋出的異常。 例如,延遲的jQuery不會 - “失敗”處理程序永遠不會得到拋出一個'then'處理程序的異常,在我看來這是一個交易斷路器。

幾天前我剛開始嘗試使用node.js。 我意識到,只要我的程序中有未處理的異常,Node就會被終止。 這與我已經接觸到的正常服務器容器不同,當只有Worker Thread在發生未處理的異常並且容器仍然能夠接收請求時死亡。 這引出了幾個問題:

  • process.on('uncaughtException')是防範它的唯一有效方法嗎?
  • process.on('uncaughtException')會在執行異步過程期間捕獲未處理的異常?
  • 是否有一個已經構建的模塊(如發送電子郵件或寫入文件),我可以利用未捕獲的異常情況下的模塊?

我將不勝感激任何能夠向我展示處理node.js中的未捕獲異常的最佳實踐的指針/文章


After reading this post some time ago I was wondering if it was safe to use domains for exception handling on an api / function level. I wanted to use them to simplify exception handling code in each async function I wrote. My concern was that using a new domain for each function would introduce significant overhead. My homework seems to indicate that there is minimal overhead and that performance is actually better with domains than with try catch in some situations.

http://www.lighthouselogic.com/#/using-a-new-domain-for-each-async-function-in-node/



以下是關於此主題的許多不同來源的摘要和策展,包括來自選定博客文章的代碼示例和引用。 這裡可以找到完整的最佳實踐列表

Node.JS錯誤處理的最佳實踐

Number1:使用承諾進行異步錯誤處理

TL; DR:處理回調風格中的異步錯誤可能是最糟糕的地獄之路(又名厄運金字塔)。 你可以給你的代碼最好的禮物是使用一個有信譽的承諾庫,它提供了非常緊湊和熟悉的代碼語法,如try-catch

否則: Node.JS回調風格,函數(err,response)是由於混合使用臨時代碼的錯誤處理,過度嵌套和難於編碼的模式而導致的不可維護代碼的一種有希望的方式

代碼示例 - 很好

doWork()
.then(doWork)
.then(doError)
.then(doWork)
.catch(errorHandler)
.then(verify);

代碼示例反模式 - 回調風格錯誤處理

getData(someParameter, function(err, result){
    if(err != null)
      //do something like calling the given callback function and pass the error
    getMoreData(a, function(err, result){
          if(err != null)
            //do something like calling the given callback function and pass the error
        getMoreData(b, function(c){ 
                getMoreData(d, function(e){ 
                    ...
                });
            });
        });
    });
});

博客引用:“我們有一個承諾問題” (來自博客pouchdb,關鍵字“節點承諾”排名11)

“......事實上,回調的做法更加險惡:他們剝奪了我們的堆棧,這在編程語言中我們通常認為是理所當然的。無需堆棧編寫代碼就像在沒有剎車踏板的情況下駕駛汽車:您沒有意識到你需要它多麼糟糕,直到你達到它並且它不在那裡。 承諾的全部重點是讓我們回到我們在異步時丟失的語言基礎:返回,拋出和堆棧。必須知道如何正確使用承諾以利用它們。

Number2:只使用內置的Error對象

TL; DR:查看將錯誤作為字符串或自定義類型拋出的代碼很常見 - 這會使錯誤處理邏輯和模塊之間的互操作性變得複雜。 無論您是否拒絕承諾,拋出異常或發出錯誤 - 使用Node.JS內置的Error對象可以提高均勻性並防止錯誤信息丟失

否則:當執行某個模塊時,不確定哪種類型的錯誤會返回 - 使得很難推斷即將發生的異常並處理它。 即使值得使用自定義類型來描述錯誤,也可能導致丟失關鍵的錯誤信息,如堆棧跟踪!

代碼示例 - 正確執行

    //throwing an Error from typical function, whether sync or async
 if(!productToAdd)
 throw new Error("How can I add new product when no value provided?");

//'throwing' an Error from EventEmitter
const myEmitter = new MyEmitter();
myEmitter.emit('error', new Error('whoops!'));

//'throwing' an Error from a Promise
 return new promise(function (resolve, reject) {
 DAL.getProduct(productToAdd.id).then((existingProduct) =>{
 if(existingProduct != null)
 return reject(new Error("Why fooling us and trying to add an existing product?"));

代碼示例反模式

//throwing a String lacks any stack trace information and other important properties
if(!productToAdd)
    throw ("How can I add new product when no value provided?");

博客引用:“字符串不是錯誤” (來自博客devthought,關鍵字“Node.JS錯誤對象”排名6)

“...傳遞一個字符串而不是錯誤會導致模塊之間的互操作性降低,它違反了可能執行instanceof錯誤檢查的API或者想了解更多關於錯誤的API 。除了將消息傳遞給構造函數之外,現代JavaScript引擎中有趣的屬性......“

Number3:區分操作與程序員錯誤

TL; DR:操作錯誤(例如,API收到無效輸入)指的是已知的錯誤影響已被充分理解並可以慎重處理的情況。 另一方面,程序員錯誤(例如嘗試讀取未定義的變量)是指未知代碼失敗,指示正常地重新啟動應用程序

否則:當出現錯誤時,您可能總是重新啟動應用程序,但為什麼~5000在線用戶因為次要和預測錯誤(操作錯誤)而關閉? 相反也不是很理想 - 當出現未知問題(程序員錯誤)時,保持應用程序可能會導致不可預測的行為。 通過區分這兩種方式,可以根據給定的上下文順利地採取平衡的方法

代碼示例 - 正確執行

    //throwing an Error from typical function, whether sync or async
 if(!productToAdd)
 throw new Error("How can I add new product when no value provided?");

//'throwing' an Error from EventEmitter
const myEmitter = new MyEmitter();
myEmitter.emit('error', new Error('whoops!'));

//'throwing' an Error from a Promise
 return new promise(function (resolve, reject) {
 DAL.getProduct(productToAdd.id).then((existingProduct) =>{
 if(existingProduct != null)
 return reject(new Error("Why fooling us and trying to add an existing product?"));

代碼示例 - 將錯誤標記為可操作(可信)

//marking an error object as operational 
var myError = new Error("How can I add new product when no value provided?");
myError.isOperational = true;

//or if you're using some centralized error factory (see other examples at the bullet "Use only the built-in Error object")
function appError(commonType, description, isOperational) {
    Error.call(this);
    Error.captureStackTrace(this);
    this.commonType = commonType;
    this.description = description;
    this.isOperational = isOperational;
};

throw new appError(errorManagement.commonErrors.InvalidInput, "Describe here what happened", true);

//error handling code within middleware
process.on('uncaughtException', function(error) {
    if(!error.isOperational)
        process.exit(1);
});

博客引用 :“否則你冒險的狀態”(從博客可調試,排名3為關鍵字“Node.JS未捕獲的異常”)

... ...由於JavaScript在JavaScript中的工作原理,幾乎沒有任何方法可以安全地”拾取你離開的地方“,而不會洩露引用,或者創建其他類型的未定義的脆弱狀態。一個拋出的錯誤是關閉進程當然,在一個普通的Web服務器中,你可能會打開很多連接,並且由於其他人觸發錯誤而突然關閉這些連接是不合理的,更好的方法是向引發錯誤的請求發送錯誤響應,同時讓其他人在正常時間內完成,並停止監聽該工作人員中的新請求“

Number4:集中處理錯誤,但不在中間件內

TL; DR:錯誤處理邏輯,如郵件發送到管理員和日誌記錄時,應封裝在一個專用的集中對像中,以便在發生錯誤時所有終端(例如Express中間件,cron作業,單元測試)都會調用。

否則:在一個地方不處理錯誤將導致代碼重複,並可能導致錯誤處理不當

代碼示例 - 一個典型的錯誤流程

//DAL layer, we don't handle errors here
DB.addDocument(newCustomer, (error, result) => {
    if (error)
        throw new Error("Great error explanation comes here", other useful parameters)
});

//API route code, we catch both sync and async errors and forward to the middleware
try {
    customerService.addNew(req.body).then(function (result) {
        res.status(200).json(result);
    }).catch((error) => {
        next(error)
    });
}
catch (error) {
    next(error);
}

//Error handling middleware, we delegate the handling to the centrzlied error handler
app.use(function (err, req, res, next) {
    errorHandler.handleError(err).then((isOperationalError) => {
        if (!isOperationalError)
            next(err);
    });
});

博客引用: “有時候,低級別無法做任何有用的事情,除了將錯誤傳播給他們的調用者”(來自Joyent博客,關鍵字“Node.JS錯誤處理”排名第一)

“......你可能最終會在堆棧的幾個層次上處理相同的錯誤,這種情況發生在較低級別除了將錯誤傳播給其調用者(將錯誤傳播給調用者)等等之外無法做任何有用的事情。只有頂級調用者知道什麼是適當的響應,無論是重試操作,向用戶報告錯誤還是其他內容,但這並不意味著您應該嘗試將所有錯誤報告給單個頂級回調,因為回調本身不知道錯誤發生在什麼情況下“

Number5:使用Swagger記錄API錯誤

TL; DR:讓您的API調用者知道哪些錯誤可能會返回,以便他們可以在不崩潰的情況下謹慎處理這些錯誤。 這通常使用Swagger等REST API文檔框架完成

否則: API客戶端可能會決定崩潰並重新啟動,因為他收到了他無法理解的錯誤。 注意:您的API的調用者可能是您(在微服務環境中非常典型)

博客引用: “你必須告訴你的調用者會發生什麼錯誤”(來自Joyent博客,關鍵字“Node.JS logging”排名第一)

...我們已經討論瞭如何處理錯誤,但是當您編寫一個新函數時,您如何向調用函數的代碼傳遞錯誤? ...如果你不知道會發生什麼錯誤或不知道他們的意思,那麼你的程序就不會是正確的,除非意外。 所以,如果你正在編寫一個新的功能,你必須告訴你的呼叫者什麼樣的錯誤會發生,他們是什麼

Number6:當一個陌生人來到城裡時,優雅地關閉這個過程

TL; DR:發生未知錯誤時(開發人員錯誤,請參閱最佳實踐編號3) - 應用程序健康性存在不確定性。 通常的做法建議使用“重啟”工具(如Forever和PM2)重新啟動該過程

否則:當一個不熟悉的異常被捕獲時,某個對象可能處於錯誤狀態(例如,由於某些內部故障,全局使用的事件發射器不再發生事件),並且所有未來的請求可能會失敗或表現得很瘋狂

代碼示例 - 決定是否崩潰

//deciding whether to crash when an uncaught exception arrives
//Assuming developers mark known operational errors with error.isOperational=true, read best practice #3
process.on('uncaughtException', function(error) {
 errorManagement.handler.handleError(error);
 if(!errorManagement.handler.isTrustedError(error))
 process.exit(1)
});


//centralized error handler encapsulates error-handling related logic 
function errorHandler(){
 this.handleError = function (error) {
 return logger.logError(err).then(sendMailToAdminIfCritical).then(saveInOpsQueueIfCritical).then(determineIfOperationalError);
 }

 this.isTrustedError = function(error)
 {
 return error.isOperational;
 }

博客引用: “關於錯誤處理有三種想法”(來自jsrecipes博客)

......關於錯誤處理主要有三種想法:1.讓應用程序崩潰並重新啟動它。 2.處理所有可能的錯誤並永不崩潰。 3.兩者平衡

Number7:使用成熟的記錄器來增加錯誤的可見性

TL; DR: Winston,Bunyan或Log4J等一套成熟的日誌工具將加速錯誤發現和理解。 所以忘了console.log。

否則:通過console.logs瀏覽或通過雜亂的文本文件手動查詢而不查詢工具或體面的日誌查看器可能會讓您忙於工作,直到遲到

代碼示例 - Winston記錄器正在運行

//your centralized logger object
var logger = new winston.Logger({
 level: 'info',
 transports: [
 new (winston.transports.Console)(),
 new (winston.transports.File)({ filename: 'somefile.log' })
 ]
 });

//custom code somewhere using the logger
logger.log('info', 'Test Log Message with some parameter %s', 'some parameter', { anything: 'This is metadata' });

博客引用: “讓我們識別一些需求(對於記錄器):”(來自博客的強博)

...讓我們識別一些要求(對於記錄器):1.時間戳記每條記錄行。 這個很自我解釋 - 你應該能夠知道每個日誌條目何時發生。 2.記錄格式應該容易被人類和機器消化。 3.允許多個可配置的目標流。 例如,您可能會將跟踪日誌寫入一個文件,但遇到錯誤時,請寫入相同的文件,然後寫入錯誤文件並同時發送電子郵件...

Number8:使用APM產品發現錯誤和停機時間

TL; DR:監控和性能產品(又名APM)主動評估您的代碼庫或API,以便它們可以自動奇蹟般突出顯示錯誤,崩潰並放慢您缺少的部分

否則:您可能會花費大量精力測量API性能和停機時間,可能您永遠不會意識到哪些是您在現實世界場景下最慢的代碼部分,以及這些影響UX的方式

博客引用: “APM產品細分”(來自Yoni Goldberg博客)

“...... APM產品構成了3個主要部分: 1.網站或API監控 -通過HTTP請求持續監控正常運行時間和性能的外部服務可以在幾分鐘內完成安裝以下幾種選擇競爭者:Pingdom,Uptime Robot和New Relic 2代碼測試 -需要在應用程序中嵌入代理以獲益的代碼測試 -產品系列包括慢代碼檢測,異常統計,性能監控等等。的產品側重於幫助ops團隊提供指標和策劃的內容,這些內容有助於輕鬆地保持應用程序的性能。這通常涉及匯總多個信息源(應用程序日誌,數據庫日誌,服務器日誌等)和前期儀表板設計以下是少數選擇的競爭者:Datadog,Splunk“

以上是縮短版本 - 請參閱此處的更多最佳做法和示例



我只想補充一點, Step.js庫通過始終將它傳遞給下一步功能來幫助您處理異常。 Therefore you can have as a last step a function that check for any errors in any of the previous steps. This approach can greatly simplify your error handling.

Below is a quote from the github page:

any exceptions thrown are caught and passed as the first argument to the next function. As long as you don't nest callback functions inline your main functions this prevents there from ever being any uncaught exceptions. This is very important for long running node.JS servers since a single uncaught exception can bring the whole server down.

Furthermore, you can use Step to control execution of scripts to have a clean up section as the last step. For example if you want to write a build script in Node and report how long it took to write, the last step can do that (rather than trying to dig out the last callback).


更新:Joyent現在在這個答案中提到了自己的指導 。 以下信息是更多摘要信息:

安全地“拋出”錯誤

理想情況下,我們希望盡可能避免未被捕獲的錯誤,因此,我們可以根據代碼體系結構使用以下方法之一安全地“拋出”錯誤,而不是直接拋出錯誤:

  • 對於同步代碼,如果發生錯誤,則返回錯誤:

    // Define divider as a syncrhonous function
    var divideSync = function(x,y) {
        // if error condition?
        if ( y === 0 ) {
            // "throw" the error safely by returning it
            return new Error("Can't divide by zero")
        }
        else {
            // no error occured, continue on
            return x/y
        }
    }
    
    // Divide 4/2
    var result = divideSync(4,2)
    // did an error occur?
    if ( result instanceof Error ) {
        // handle the error safely
        console.log('4/2=err', result)
    }
    else {
        // no error occured, continue on
        console.log('4/2='+result)
    }
    
    // Divide 4/0
    result = divideSync(4,0)
    // did an error occur?
    if ( result instanceof Error ) {
        // handle the error safely
        console.log('4/0=err', result)
    }
    else {
        // no error occured, continue on
        console.log('4/0='+result)
    }
    
  • 對於基於回調的(即異步)代碼,回調的第一個參數是err ,如果發生err是錯誤,如果沒有發生錯誤,則errnull 。 任何其他參數遵循err參數:

    var divide = function(x,y,next) {
        // if error condition?
        if ( y === 0 ) {
            // "throw" the error safely by calling the completion callback
            // with the first argument being the error
            next(new Error("Can't divide by zero"))
        }
        else {
            // no error occured, continue on
            next(null, x/y)
        }
    }
    
    divide(4,2,function(err,result){
        // did an error occur?
        if ( err ) {
            // handle the error safely
            console.log('4/2=err', err)
        }
        else {
            // no error occured, continue on
            console.log('4/2='+result)
        }
    })
    
    divide(4,0,function(err,result){
        // did an error occur?
        if ( err ) {
            // handle the error safely
            console.log('4/0=err', err)
        }
        else {
            // no error occured, continue on
            console.log('4/0='+result)
        }
    })
    
  • 對於eventful代碼,錯誤可能發生在任何地方,而不是拋出錯誤, 而是觸發error事件

    // Definite our Divider Event Emitter
    var events = require('events')
    var Divider = function(){
        events.EventEmitter.call(this)
    }
    require('util').inherits(Divider, events.EventEmitter)
    
    // Add the divide function
    Divider.prototype.divide = function(x,y){
        // if error condition?
        if ( y === 0 ) {
            // "throw" the error safely by emitting it
            var err = new Error("Can't divide by zero")
            this.emit('error', err)
        }
        else {
            // no error occured, continue on
            this.emit('divided', x, y, x/y)
        }
    
        // Chain
        return this;
    }
    
    // Create our divider and listen for errors
    var divider = new Divider()
    divider.on('error', function(err){
        // handle the error safely
        console.log(err)
    })
    divider.on('divided', function(x,y,result){
        console.log(x+'/'+y+'='+result)
    })
    
    // Divide
    divider.divide(4,2).divide(4,0)
    

安全地“捕捉”錯誤

有時候,如果我們沒有安全地捕捉到它,可能仍會有代碼在某處引發錯誤,導致未捕獲的異常和應用程序崩潰。 根據我們的代碼體系結構,我們可以使用以下方法之一來捕捉它:

  • 當我們知道錯誤發生的位置時,我們可以將該節包裝在node.js域中

    var d = require('domain').create()
    d.on('error', function(err){
        // handle the error safely
        console.log(err)
    })
    
    // catch the uncaught errors in this asynchronous or synchronous code block
    d.run(function(){
        // the asynchronous or synchronous code that we want to catch thrown errors on
        var err = new Error('example')
        throw err
    })
    
  • 如果我們知道錯誤發生的地方是同步代碼,並且出於任何原因不能使用域(可能是舊版本的節點),我們可以使用try catch語句:

    // catch the uncaught errors in this synchronous code block
    // try catch statements only work on synchronous code
    try {
        // the synchronous code that we want to catch thrown errors on
        var err = new Error('example')
        throw err
    } catch (err) {
        // handle the error safely
        console.log(err)
    }
    

    但是,請注意不要在異步代碼中使用try...catch ,因為異步拋出的錯誤不會被捕獲:

    try {
        setTimeout(function(){
            var err = new Error('example')
            throw err
        }, 1000)
    }
    catch (err) {
        // Example error won't be caught here... crashing our app
        // hence the need for domains
    }
    

    另一件需要注意的事情是try...catch是在try語句中包含完成回調的風險,如下所示:

    var divide = function(x,y,next) {
        // if error condition?
        if ( y === 0 ) {
            // "throw" the error safely by calling the completion callback
            // with the first argument being the error
            next(new Error("Can't divide by zero"))
        }
        else {
            // no error occured, continue on
            next(null, x/y)
        }
    }
    
    var continueElsewhere = function(err, result){
            throw new Error('elsewhere has failed')
    }
    
    try {
            divide(4, 2, continueElsewhere)
            // ^ the execution of divide, and the execution of 
            //   continueElsewhere will be inside the try statement
    }
    catch (err) {
            console.log(err.stack)
            // ^ will output the "unexpected" result of: elsewhere has failed
    }
    

    隨著代碼變得越來越複雜,這個問題很容易實現。 因此,最好使用域或返回錯誤以避免(1)異步代碼中未捕獲的異常(2)您不希望捕獲執行的try catch。 在允許正確線程代替JavaScript異步事件機器風格的語言中,這不是一個問題。

  • 最後,如果未被捕獲的錯誤發生在未包含在域或try catch語句中的地方,我們可以通過使用uncaughtException偵聽器來使應用程序不會崩潰(但這樣做可能會使應用程序處於未知狀態狀態 ):

    // catch the uncaught errors that weren't wrapped in a domain or try catch statement
    // do not use this in modules, but only in applications, as otherwise we could have multiple of these bound
    process.on('uncaughtException', function(err) {
        // handle the error safely
        console.log(err)
    })
    
    // the asynchronous or synchronous code that emits the otherwise uncaught error
    var err = new Error('example')
    throw err
    




serverside-javascript