try Node.js最佳實踐異常處理




node js try catch (8)

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

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

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


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

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“

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



在使用forEach循環時,使用try-catch的一個實例可能比較合適。 它是同步的,但同時你不能在內部範圍內使用return語句。 相反,try和catch方法可用於在適當範圍內返回Error對象。 考慮:

function processArray() {
    try { 
       [1, 2, 3].forEach(function() { throw new Error('exception'); }); 
    } catch (e) { 
       return e; 
    }
}

它是上述@balupton描述的方法的組合。



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/


我最近在http://snmaynard.com/2012/12/21/node-error-handling/寫了這篇文章。 版本0.8中的一個新功能是域,並允許您將所有形式的錯誤處理組合成一個更容易管理的表單。 你可以在我的文章中閱讀關於他們。

您還可以使用Bugsnag之類的功能來追踪未捕獲的異常,並通過電子郵件,聊天室通知或為未捕獲的異常創建一張票(我是Bugsnag的聯合創始人)。


Catching errors has been very well discussed here, but it's worth remembering to log the errors out somewhere so you can view them and fix stuff up.

​Bunyan is a popular logging framework for NodeJS - it supporst writing out to a bunch of different output places which makes it useful for local debugging, as long as you avoid console.log. ​ In your domain's error handler you could spit the error out to a log file.

var log = bunyan.createLogger({
  name: 'myapp',
  streams: [
    {
      level: 'error',
      path: '/var/tmp/myapp-error.log'  // log ERROR to this file
    }
  ]
});

如果您有很多錯誤和/或服務器需要檢查,這可能會耗費大量時間,因此可能需要查看像Raygun(免責聲明,我在Raygun工作)的工具,將錯誤組合在一起 - 或將它們一起使用。如果你決定使用Raygun作為工具,那麼安裝起來也很容易

var raygunClient = new raygun.Client().init({ apiKey: 'your API key' });
raygunClient.send(theError);

與使用像PM2一樣的工具或永遠交叉,你的應用程序應該能夠崩潰,註銷發生了什麼,並重新啟動,沒有任何重大問題。


我只想補充一點, 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).





serverside-javascript