NodeJS处理Express中异步错误
摘要
比起回调函数,使用Promise来处理异步错误要显得优雅许多。
结合Express内置的错误处理机制和Promise极大地降低产生未捕获错误(uncaughtexception)的可能性。
Promise在ES6中是默认选项。如果使用Babel转译,它也可以与Generators或者Async/Await相结合。
本文主要阐述如何在Express中使用错误处理中间件(error-handlingmiddleware)来高效处理异步错误。在Github上有对应代码实例可供参考。
首先,让我们一起了解Express提供的开箱即用的错误处理工具。然后,我们将探讨如何使用Promise,Generators以及ES7的async/await来简化错误处理流程。
Express内置的异步错误处理
在默认情况下,Express会捕获所有在路由处理函数中的抛出的异常,然后将它传给下一个错误处理中间件:
app.get('/',function(req,res){ thrownewError('ohno!') }) app.use(function(err,req,res,next){ console.log(err.message)//噢!不! })
对于同步执行的代码,以上的处理已经足够简单。然而,当异步程序在执行时抛出异常的情况,Express就无能为力。原因在于当你的程序开始执行回调函数时,它原来的栈信息已经丢失。
app.get('/',function(req,res){ queryDb(function(er,data){ if(er)thrower }) }) app.use(function(err,req,res,next){ //这里拿不到错误信息 })
对于这种情况,可以使用next函数来将错误传递给下一个错误处理中间件
app.get('/',function(req,res,next){ queryDb(function(err,data){ if(err)returnnext(err) //处理数据 makeCsv(data,function(err,csv){ if(err)returnnext(err) //处理csv }) }) }) app.use(function(err,req,res,next){ //处理错误 })
使用这种方法虽然一时爽,却带来了两个问题:
你需要显式地在错误处理中间件中分别处理不同的异常。
一些隐式异常并没有被处理(如尝试获取一个对象并不存在的属性)
利用Promise传递异步错误
在异步执行的程序中使用Promise处理任何显式或隐式的异常情况,只需要在Promise链尾加上.catch(next)即可。
app.get('/',function(req,res,next){ //dosomesyncstuff queryDb() .then(function(data){ //处理数据 returnmakeCsv(data) }) .then(function(csv){ //处理csv }) .catch(next) }) app.use(function(err,req,res,next){ //处理错误 })
现在,所有异步和同步程序都将被传递到错误处理中间件。棒棒的。
虽然Promise让异步错误的传递变得容易,但这样的代码仍然有一些冗长和刻板。这时候promisegenerator就派上了用场。
用Generators简化代码
如果你使用的环境原生支持Generators,你可以手动实现以下的功能。不过这里我们将借用Bluebird.coroutine来说明如何使用Promisegenerator来简化刚才的代码。
尽管接下来的例子使用的是bluebird,其它Promise库(如co)也都支持Promisegenerator.
首先,我们需要使得Express路由函数与Promisegenerator兼容:
varPromise=require('bluebird') functionwrap(genFn){//1 varcr=Promise.coroutine(genFn)//2 returnfunction(req,res,next){//3 cr(req,res,next).catch(next)//4 } }
这个函数是一个高阶函数,它做了以下几件事情:(分别与代码片段中的注释对应)
以Genrator为唯一的输入
让这个函数懂得如何yieldpromise
返回一个普通的Express路由函数
当这个函数被执行时,它会使用coroutine来yieldpromise,捕获期间发生的异常,然后将其传递给next函数
借助这个函数,我们就可以这样构造路由函数:
app.get('/',wrap(function*(req,res){ vardata=yieldqueryDb() //处理数据 varcsv=yieldmakeCsv(data) //处理csv })) app.use(function(err,req,res,next){ //处理错误 })
现在,Express的异步错误处理流程的可读性已经近乎令人满意,而且你可以像写同步执行的代码一样去书写异步执行的代码,唯一不要忘了的就是yieldpromises。
然而这还不是终点,ES7的async/await提议可以让代码变得更简洁。
使用ES7async/await
ES7async/await的行为就像PromiseGenerator一样,只不过它可以被用到更多的地方(如类方法或者胖箭头函数)。
为了在Express中使用async/await,同时优雅地处理异步错误,我们仍然需要一个与上文提到的wrap类似的函数:
letwrap=fn=>(...args)=>fn(...args).catch(args[2])
这样,我们就可以按底下这种方式书写路由函数:
app.get('/',wrap(asyncfunction(req,res){ letdata=awaitqueryDb() //处理数据 letcsv=awaitmakeCsv(data) //处理csv }))
现在可以愉快地写代码了
有了对同步和异步错误的处理,你可以用新的方式来开发ExpressApp。但有两点需要注意:
要习惯使用throw,它使得你的代码目的明确,throw会明确地将程序引到错误处理中间件,这对同步或异步的程序都是适用的。
遇到特殊情况,当你觉得有必要时,也可以自行try/catch。
app.get('/',wrap(async(req,res)=>{ if(!req.params.id){ thrownewBadRequestError('MissingId') } letcompanyLogo try{ companyLogo=awaitgetBase64Logo(req.params.id) }catch(err){ console.error(err) companyLogo=genericBase64Logo } }))
要习惯使用customerrorclasses,如BadRequestError,因为这可以让你在错误处理中间件中更方便地分类处理。
app.use(function(err,req,res,next){ if(errinstanceofBadRequestError){ res.status(400) returnres.send(err.message) } ... })
需要注意
- 以上介绍的方法要求所有异步操作必须返回promise。如果你的异步操作是使用回调函数的方式,你需要将其转化成promise。(可以直接使用Bluebird.promisifyAll这类函数)
- 事件发射器(如steams)仍然会导致未捕获异常,你需要注意合理地处理这类情况: