NodeJS處理Express中異步錯誤
摘要
比起回調(diào)函數(shù),使用 Promise 來處理異步錯誤要顯得優(yōu)雅許多。
結(jié)合 Express 內(nèi)置的錯誤處理機制和 Promise 極大地降低產(chǎn)生未捕獲錯誤(uncaught exception)的可能性。
Promise 在ES6中是默認(rèn)選項。如果使用 Babel 轉(zhuǎn)譯,它也可以與 Generators 或者 Async/Await 相結(jié)合。
本文主要闡述如何在 Express 中使用錯誤處理中間件(error-handling middleware)來高效處理異步錯誤。在 Github 上有對應(yīng) 代碼實例 可供參考。
首先,讓我們一起了解 Express 提供的開箱即用的錯誤處理工具。然后,我們將探討如何使用 Promise, Generators 以及 ES7 的 async/await 來簡化錯誤處理流程。
Express 內(nèi)置的異步錯誤處理
在默認(rèn)情況下,Express 會捕獲所有在路由處理函數(shù)中的拋出的異常,然后將它傳給下一個錯誤處理中間件:
app.get('/', function (req, res) { throw new Error('oh no!') }) app.use(function (err, req, res, next) { console.log(err.message) // 噢!不! })
對于同步執(zhí)行的代碼,以上的處理已經(jīng)足夠簡單。然而,當(dāng)異步程序在執(zhí)行時拋出異常的情況,Express 就無能為力。原因在于當(dāng)你的程序開始執(zhí)行回調(diào)函數(shù)時,它原來的棧信息已經(jīng)丟失。
app.get('/', function (req, res) { queryDb(function (er, data) { if (er) throw er }) }) app.use(function (err, req, res, next) { // 這里拿不到錯誤信息 })
對于這種情況,可以使用 next 函數(shù)來將錯誤傳遞給下一個錯誤處理中間件
app.get('/', function (req, res, next) { queryDb(function (err, data) { if (err) return next(err) // 處理數(shù)據(jù) makeCsv(data, function (err, csv) { if (err) return next(err) // 處理 csv }) }) }) app.use(function (err, req, res, next) { // 處理錯誤 })
使用這種方法雖然一時爽,卻帶來了兩個問題:
你需要顯式地在錯誤處理中間件中分別處理不同的異常。
一些隱式異常并沒有被處理(如嘗試獲取一個對象并不存在的屬性)
利用 Promise 傳遞異步錯誤
在異步執(zhí)行的程序中使用 Promise 處理任何顯式或隱式的異常情況,只需要在 Promise 鏈尾加上 .catch(next) 即可。
app.get('/', function (req, res, next) { // do some sync stuff queryDb() .then(function (data) { // 處理數(shù)據(jù) return makeCsv(data) }) .then(function (csv) { // 處理 csv }) .catch(next) }) app.use(function (err, req, res, next) { // 處理錯誤 })
現(xiàn)在,所有異步和同步程序都將被傳遞到錯誤處理中間件。棒棒的。
雖然 Promise 讓異步錯誤的傳遞變得容易,但這樣的代碼仍然有一些冗長和刻板。這時候 promise generator 就派上了用場。
用 Generators 簡化代碼
如果你使用的環(huán)境原生支持 Generators,你可以手動實現(xiàn)以下的功能。不過這里我們將借用 Bluebird.coroutine 來說明如何使用 Promise generator 來簡化剛才的代碼。
盡管接下來的例子使用的是 bluebird ,其它 Promise 庫(如 co)也都支持 Promise generator.
首先,我們需要使得 Express 路由函數(shù)與 Promise generator 兼容:
var Promise = require('bluebird') function wrap (genFn) { // 1 var cr = Promise.coroutine(genFn) // 2 return function (req, res, next) { // 3 cr(req, res, next).catch(next) // 4 } }
這個函數(shù)是一個高階函數(shù),它做了以下幾件事情:(分別與代碼片段中的注釋對應(yīng))
以 Genrator 為唯一的輸入
讓這個函數(shù)懂得如何 yield promise
返回一個普通的 Express 路由函數(shù)
當(dāng)這個函數(shù)被執(zhí)行時,它會使用 coroutine 來 yield promise,捕獲期間發(fā)生的異常,然后將其傳遞給 next 函數(shù)
借助這個函數(shù),我們就可以這樣構(gòu)造路由函數(shù):
app.get('/', wrap(function *(req, res) { var data = yield queryDb() // 處理數(shù)據(jù) var csv = yield makeCsv(data) // 處理 csv })) app.use(function (err, req, res, next) { // 處理錯誤 })
現(xiàn)在,Express 的異步錯誤處理流程的可讀性已經(jīng)近乎令人滿意,而且你可以像寫同步執(zhí)行的代碼一樣去書寫異步執(zhí)行的代碼,唯一不要忘了的就是 yield promises。
然而這還不是終點,ES7 的 async/await 提議可以讓代碼變得更簡潔。
使用 ES7 async/await
ES7 async/await 的行為就像 Promise Generator 一樣,只不過它可以被用到更多的地方(如類方法或者胖箭頭函數(shù))。
為了在 Express 中使用 async/await,同時優(yōu)雅地處理異步錯誤,我們?nèi)匀恍枰粋€與上文提到的 wrap 類似的函數(shù):
let wrap = fn => (...args) => fn(...args).catch(args[2])
這樣,我們就可以按底下這種方式書寫路由函數(shù):
app.get('/', wrap(async function (req, res) { let data = await queryDb() // 處理數(shù)據(jù) let csv = await makeCsv(data) // 處理 csv }))
現(xiàn)在可以愉快地寫代碼了
有了對同步和異步錯誤的處理,你可以用新的方式來開發(fā) Express App。但有兩點需要注意:
要習(xí)慣使用 throw ,它使得你的代碼目的明確,throw 會明確地將程序引到錯誤處理中間件,這對同步或異步的程序都是適用的。
遇到特殊情況,當(dāng)你覺得有必要時,也可以自行 try/catch。
app.get('/', wrap(async (req, res) => { if (!req.params.id) { throw new BadRequestError('Missing Id') } let companyLogo try { companyLogo = await getBase64Logo(req.params.id) } catch (err) { console.error(err) companyLogo = genericBase64Logo } }))
要習(xí)慣使用 custom error classes ,如 BadRequestError,因為這可以讓你在錯誤處理中間件中更方便地分類處理。
app.use(function (err, req, res, next) { if (err instanceof BadRequestError) { res.status(400) return res.send(err.message) } ... })
需要注意
- 以上介紹的方法要求所有異步操作必須返回 promise。如果你的異步操作是使用回調(diào)函數(shù)的方式,你需要將其轉(zhuǎn)化成 promise。(可以直接使用 Bluebird.promisifyAll 這類函數(shù))
- 事件發(fā)射器(如 steams)仍然會導(dǎo)致未捕獲異常,你需要注意合理地處理這類情況:
相關(guān)文章
node項目使用http模塊發(fā)送get-post請求方式
這篇文章主要介紹了node項目使用http模塊發(fā)送get-post請求方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-09-09Node.js里面的內(nèi)置模塊和自定義模塊的實現(xiàn)
這篇文章主要介紹了Node.js里面的內(nèi)置模塊和自定義模塊的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05使用cluster 將自己的Node服務(wù)器擴展為多線程服務(wù)器
nodejs在v0.6.x之后 增加了一個模塊 cluster 用于實現(xiàn)多進程,利用child_process模塊來創(chuàng)建和管理進程,增加程序在多核CPU機器上的性能表現(xiàn)。本文將介紹利用cluster模塊創(chuàng)建的多線程的問題。2014-11-11利用n 升級工具升級Node.js版本及在mac環(huán)境下的坑
這篇文章主要介紹了利用n 升級工具升級Node.js的方法,以及通過網(wǎng)友的測試發(fā)現(xiàn)在mac環(huán)境下利用n工具升級不成功導(dǎo)致node.js不可用的解決方法,有需要的朋友可以參考借鑒,下面來一起看看吧。2017-02-02node.js利用redis數(shù)據(jù)庫緩存數(shù)據(jù)的方法
Redis數(shù)據(jù)庫采用極簡的設(shè)計思想,最新版的源碼包還不到2Mb。其在使用上也有別于一般的數(shù)據(jù)庫。下面這篇文章就來給大家介紹了node.js利用redis數(shù)據(jù)庫緩存數(shù)據(jù)的方法,需要的朋友可以參考借鑒,下面來一起看看吧。2017-03-03