当我们构建一个web应用时,我们通常都希望它能够足够稳定,最起码不会出现大面积的服务不可用的情况,而nodejs是单进程的,虽然现在也可以很方便的部署多个进程(多实例),但是每个进程也是会承载多个用户的请求。所以,在node.js中的错误处理尤为重要,而异步虽然能够极大的提升node.js处理请求的性能,但是同时也让错误处理变得复杂。
本文最后会回答大家两个问题?
- 如何更好的处理异步?
- 如何构建更健壮的应用,不会因为单个用户的请求error,让整个应用crash?
建议大家可以把源码下载下来运行体验,本文涉及到的源码下载地址 https://github.com/elover/async_show.git
本文主要通过几个简单的web应用来演示node.js中的异步流程控制和错误处理,来更形象的说明各种异步处理的优缺点,之所以,通过web应用的方式来演示,是为了更好的认识到错误处理的重要性。
1、callback function
通过异步回调来处理异步操作可能是目前nodejs中使用最广泛的方案。通过约定回调函数中第一个参数,为error对象,来传递错误,当第一个参数为null时表示没有错误。为什么要这么做呢,因为异步中的错误是无法通过try catch捕获的。
var express = require('express')
var app = express()
app.get('/', function (req, res, next) {
async1(function (err, data1) {
if (err) {
return res.send(err.message);
}
async2(function (err, data2) {
if (err) {
return res.send(err.message);
}
res.send(data1 + data2);
});
});
});
// 异步1
function async1(fn) {
//throw Error('error') //[@1](/user/1) 可捕获的异常
setTimeout(function () {
// throw Error('error') //[@2](/user/2) 异步中的异常 不能捕获,会直接导致应用crash退出
fn(null, 'Hello '); //[@3](/user/3) 正常
//fn(Error('error'), 1); //[@4](/user/4) 异常
}, 1)
}
// 异步2
function async2(fn) {
//throw Error('error') //[@5](/user/5) 异步中的异常 不能捕获,会直接导致应用crash退出
setTimeout(function () {
fn(null, 'World')
//fn(Error('error'));
}, 1)
}
app.listen(4000, function () {
console.log('node listen localhost:4000');
});
上面代码是个很简单的通过callback来传递错误的示例,注释中有5个标记@1-@5。
当我们把@1的注释去掉,请求http://localhost:4000/ 时,页面会显示错误信息,但这个错误并不会造成应用crash掉,应用还能处理其他请求,因为这里的throw Error,并不是发生在异步中,所以express会帮助我们捕获到这个错误(实际也是通过try catch捕获的),应用不会crash。
当我们把@2的注释去掉,请求http://localhost:4000/ 时,此时应用就会立刻crash掉,不会再处理任何请求,进程终止。为什么呢,因为这里的throw Error是发生在异步中,无法捕获,所以应用会直接报错退出。
@3和@4是错误传递的方式,可以将错误传递给上层调用者,错误能够被上次处理。
去掉@5的注释后,请求http://localhost:4000/ 时,应用crash,why?大家可能会觉得,为什么@1处的错误能捕获,到了@5的错误却不能捕获呢,这是应该,async2是在async1中调用的,所以,这里的throw error,其实是发生在异步中。
由此可见,通过回调函数,来处理异步,不是一件很优雅的方法,不仅会造成嵌套层级太多,而且,对错误的处理也不够好,当然,使用async这个库,也可以减少很多麻烦。
2、promise
promise 也是处理异步的方法之一,目前,已经是一种规范,并且被node.js原生实现。
var express = require('express')
var app = express()
app.get('/', function (req, res, next) {
promise1()
.then(function (data1) {
//throw Error('error');//[@1](/user/1) 捕获
return promise2(data1)
})
.then(function (data2) {
res.send(data2);
})
.catch(function (e) {
res.send(e.message);
});
});
//promise1
function promise1() {
return new Promise(function (resole, reject) {
//throw Error('error'); //[@2](/user/2) 捕获
setTimeout(function () {
//throw Error('error'); //[@3](/user/3) crash
resole("Hello"); //[@4](/user/4) 返回正确结果
//reject(Error('error));//[@5](/user/5) 返回错误结果
}, 1)
})
}
//promise2
function promise2(data1) {
return new Promise(function (resole, reject) {
setTimeout(function () {
resole(data1 + " world");
}, 1)
}, 1)
}
app.listen(5000, function () {
console.log('node listen localhost:5000');
});
在这里,@1、@2,@5错误能够被捕获,而@3处的错误,却能够造成应用的crash,还是一样的,即使在promise中,异步的错误也是不能捕获的,只能通过reject来传递。
相对于callback,利用promise来处理异步可能要优雅一些,但同时,也会造成一定的困扰,比如,会让你的代码像面条一样穿插了非常多的then方法,因为通常用这种方式处理异步后,你的绝大部分代码都需要返回promise,并且通过then来连接起来。
3、 generator function(co)
通过co来实现异步处理,co是一个通过包装generator function来处理异步流的库。为了方便,这里使用koa来做演示,在express中使用co这个库也是类似的。只是koa中,已经集成进去了。
var koa = require("koa");
var app = koa();
// 调用异步函数
app.use(function *(next) {
var getPromise = yield promise();
var getThunks = yield thunks();
this.body = getPromise + getThunks;
});
// promise 推荐
function promise() {
return new Promise(function (resolve, reject) {
//throw Error('error') //[@1](/user/1) 可捕获的异常
setTimeout(function () {
//throw Error('error') //[@2](/user/2) 异步中的异常,不可捕获的异常
resolve("Hello");
}, 1);
})
}
// thunks function 不推荐
function thunks() {
return function (fn) {
//throw Error('error') //[@3](/user/3) 可捕获的异常
setTimeout(function () {
fn(null, " world");//[@4](/user/4) 正常
//fn(Error('error'), 1); //[@5](/user/5) 异常
})
}
}
app.listen(3000, function () {
console.log('node listen localhost:3000');
});
yield 支持多种形式,这里近演示promise和thunks function,推荐promise。 其中@1,@3,@5异常可以捕获,@2会造成程序的crash。
大家可以看到,在这种方式中,通过yield来调用异步函数,和大家平常写同步代码类似代码,但代码执行却依旧是异步的,怎么实现的呢,这里主要是通过generator function的特性来实现的,感兴趣的也可以去了解下,目前 node 4.0及以上版本已经实现。 而且只要是支持返回promise 的,都可以直接使用,唯一的不足时,需要使用到generator,和一般的function并不兼容,并且通过generator处理异步,有种hack的感觉。
4、async await function(es2016 规范)
引入babel,转换成node.js支持的代码
require('babel/register')({ stage: 0 }); // 依赖babel
var app = require('./app');
app.listen(7000, function () {
console.log('node listen localhost:7000');
});
主程序代码,引入babel时,必须分成两个文件,因为通过require进去的代码,才会被babel转换。
var express = require('express');
var app = express();
app.get('/', async function (req, res, next) {
try{
var getAsync1 = await async1();
var getAsync2 = await async2();
res.send(getAsync1 + getAsync2);
}catch (e){
console.log(e);
res.send(e.message);
}
});
async function async1() {
return new Promise(function (resolve, reject) {
//throw Error('error1'); // [@1](/user/1) 捕获
setTimeout(function () {
//throw Error('error1'); // [@2](/user/2) crash
resolve("Hello");
}, 1);
});
}
// 异步1
async function async2() {
return new Promise(function (resolve, reject) {
//throw Error('error1'); // [@3](/user/3) 捕获
setTimeout(function () {
resolve(" World");
}, 1);
});
}
module.exports = app;
为了更好的处理异步,es2016加入了关键词async await,和上面的generator+promise很类似,因为async await实现方式本身也是基于generator+promise,只是目前而言,node.js中别没有实现,所以只能借助babel来转化成node.js支持的语法,而就目前来讲,在node.js中加入babel依赖,并不是比较好的事情,一来会很影响性能,二来对代码调试也会造成一定的影响。
综上,回答上面那两个问题:
1、就目前来讲,在async await还没有被node.js实现的情况下,通过第三种方案,应该是最好的方案,实际上第三种方案中只要yield接受的类型是promise,那么和方案四基本上是类似的,最后只用将generator function换成async function,yield换成await即可。同时目前,async await并不支持多个异步并发,而co库里面是支持的。 2、我们上面的例子中演示的,不管采用那种方式,我们都不能处理异步中抛出的异常,异步中的异常只能通过传递来传递给调用者,所以,在异步中应该尽可能的少做逻辑,只是作为必要的操作等,比如,我们可以通过异步来获取数据,但数据的格式化等,则放到异步之外(也就是上面演示中的setTimeout之外),这样的话,绝大部分的异常都能被捕获,应用crash的几率也会大大的减少。
最后,尽管我们能捕获大部分错误,但是我们还是应该把捕获到的异常进行记录分析,尽可能减少异常,并且,通过完善的单元测试来事先发现问题。
domain是不是可以配合用上
话说promise错误是冒泡了然而发现经常不知道从哪里冒出来的。。。
await 写错啦~ orz…
@magicdawn 谢谢提醒,已经改过来啦
@reverland promise支持多个catch,能有助于错误定位