nodejs的异步和错误处理
发布于 4 年前 作者 elover 7508 次浏览 最后一次编辑是 3 年前 来自 分享

当我们构建一个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来传递。

@4@5分别用来返回正确的异步结果和错误。

相对于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的几率也会大大的减少。

最后,尽管我们能捕获大部分错误,但是我们还是应该把捕获到的异常进行记录分析,尽可能减少异常,并且,通过完善的单元测试来事先发现问题。

6 回复

顺便问个问题,如何避免文章中的@1被转换成@1

domain是不是可以配合用上

话说promise错误是冒泡了然而发现经常不知道从哪里冒出来的。。。

await 写错啦~ orz…

@magicdawn 谢谢提醒,已经改过来啦

@reverland promise支持多个catch,能有助于错误定位

@elover 多个catch?

a.catch(e=>console.error(‘a:’+e))

a.then(b).catch(e=>console.error(‘b:’+e);

这种?

回到顶部