翻譯&編輯/鶴爺
原文/Marc Harter
摘要
比起回撥函式,使用 Promise 來處理非同步錯誤要顯得優雅許多。
結合 Express 內建的錯誤處理機制和 Promise 極大地降低產生未捕獲錯誤(uncaught exception)的可能性。
Promise 在ES6中是預設選項。如果使用 Babel 轉譯,它也可以與 Generators 或者 Async/Await 相結合。
本文主要闡述如何在 Express 中使用錯誤處理中介軟體(error-handling middleware)來高效處理非同步錯誤。在 Github 上有對應程式碼例項可供參考。
首先,讓我們一起了解 Express 提供的開箱即用的錯誤處理工具。然後,我們將探討如何使用 Promise, Generators 以及 ES7 的 async/await 來簡化錯誤處理流程。
Express 內建的非同步錯誤處理
在預設情況下,Express 會捕獲所有在路由處理函式中的丟擲的異常,然後將它傳給下一個錯誤處理中介軟體:
app.get('/', function (req, res) {
throw new Error('oh no!')
})
app.use(function (err, req, res, next) {
console.log(err.message) // 噢!不!
})複製程式碼
對於同步執行的程式碼,以上的處理已經足夠簡單。然而,當非同步程式在執行時丟擲異常的情況,Express 就無能為力。原因在於當你的程式開始執行回撥函式時,它原來的棧資訊已經丟失。
app.get('/', function (req, res) {
queryDb(function (er, data) {
if (er) throw er
})
})
app.use(function (err, req, res, next) {
// 這裡拿不到錯誤資訊
})複製程式碼
對於這種情況,可以使用 next 函式來將錯誤傳遞給下一個錯誤處理中介軟體
app.get('/', function (req, res, next) {
queryDb(function (err, data) {
if (err) return next(err)
// 處理資料
makeCsv(data, function (err, csv) {
if (err) return next(err)
// 處理 csv
})
})
})
app.use(function (err, req, res, next) {
// 處理錯誤
})複製程式碼
使用這種方法雖然一時爽,卻帶來了兩個問題:
你需要顯式地在錯誤處理中介軟體中分別處理不同的異常。
一些隱式異常並沒有被處理(如嘗試獲取一個物件並不存在的屬性)
利用 Promise 傳遞非同步錯誤
在非同步執行的程式中使用 Promise 處理任何顯式或隱式的異常情況,只需要在 Promise 鏈尾加上 .catch(next) 即可。
app.get('/', function (req, res, next) {
// do some sync stuff
queryDb()
.then(function (data) {
// 處理資料
return makeCsv(data)
})
.then(function (csv) {
// 處理 csv
})
.catch(next)
})
app.use(function (err, req, res, next) {
// 處理錯誤
})複製程式碼
現在,所有非同步和同步程式都將被傳遞到錯誤處理中介軟體。棒棒的。
雖然 Promise 讓非同步錯誤的傳遞變得容易,但這樣的程式碼仍然有一些冗長和刻板。這時候 promise generator 就派上了用場。
用 Generators 簡化程式碼
如果你使用的環境原生支援 Generators,你可以手動實現以下的功能。不過這裡我們將借用 Bluebird.coroutine 來說明如何使用 Promise generator 來簡化剛才的程式碼。
儘管接下來的例子使用的是 bluebird ,其它 Promise 庫(如 co)也都支援 Promise generator.
首先,我們需要使得 Express 路由函式與 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
}
}複製程式碼
這個函式是一個高階函式,它做了以下幾件事情:(分別與程式碼片段中的註釋對應)
以 Genrator 為唯一的輸入
讓這個函式懂得如何 yield promise
返回一個普通的 Express 路由函式
當這個函式被執行時,它會使用 coroutine 來 yield promise,捕獲期間發生的異常,然後將其傳遞給 next 函式
藉助這個函式,我們就可以這樣構造路由函式:
app.get('/', wrap(function *(req, res) {
var data = yield queryDb()
// 處理資料
var csv = yield makeCsv(data)
// 處理 csv
}))
app.use(function (err, req, res, next) {
// 處理錯誤
})複製程式碼
現在,Express 的非同步錯誤處理流程的可讀性已經近乎令人滿意,而且你可以像寫同步執行的程式碼一樣去書寫非同步執行的程式碼,唯一不要忘了的就是 yield promises。
然而這還不是終點,ES7 的 async/await 提議可以讓程式碼變得更簡潔。
使用 ES7 async/await
ES7 async/await 的行為就像 Promise Generator 一樣,只不過它可以被用到更多的地方(如類方法或者胖箭頭函式)。
為了在 Express 中使用 async/await,同時優雅地處理非同步錯誤,我們仍然需要一個與上文提到的 wrap 類似的函式:
let wrap = fn => (...args) => fn(...args).catch(args[2])複製程式碼
這樣,我們就可以按底下這種方式書寫路由函式:
app.get('/', wrap(async function (req, res) {
let data = await queryDb()
// 處理資料
let csv = await makeCsv(data)
// 處理 csv
}))複製程式碼
現在可以愉快地寫程式碼了
有了對同步和非同步錯誤的處理,你可以用新的方式來開發 Express App。但有兩點需要注意:
- 要習慣使用 throw ,它使得你的程式碼目的明確,throw 會明確地將程式引到錯誤處理中介軟體,這對同步或非同步的程式都是適用的。
- 遇到特殊情況,當你覺得有必要時,也可以自行 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
}
}))複製程式碼
- 要習慣使用 custom error classes,如 BadRequestError,因為這可以讓你在錯誤處理中介軟體中更方便地分類處理。
app.use(function (err, req, res, next) {
if (err instanceof BadRequestError) {
res.status(400)
return res.send(err.message)
}
...
})複製程式碼
需要注意
- 以上介紹的方法要求所有非同步操作必須返回 promise。如果你的非同步操作是使用回撥函式的方式,你需要將其轉化成 promise。(可以直接使用 Bluebird.promisifyAll 這類函式)
事件發射器(如 steams)仍然會導致未捕獲異常,你需要注意合理地處理這類情況:
```
原連結地址 馬達資料官方公眾平臺, Strong Loop