异步编程中的异常处理

2015-08-10 Alex Sun 更多博文 » 博客 » GitHub »

原文链接 https://syaning.github.io/2015/08/10/asynchronous-error-handling/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


一般情况下,我们会使用try..catch..来进行异常处理,例如:

function sync() {
    throw new Error('sync error');
}

try {
    sync();
} catch (err) {
    console.log('error caught:', err.message);
}

// error caught: sync error

然而对于异步函数,try..catch..就无法得到想要的效果了,例如:

function async() {
    setTimeout(function() {
        throw new Error('async error');
    }, 1000);
}

try {
    async();
} catch (err) {
    console.log('error caught:', err.message);
}

// Uncaught Error: async error

这是因为异步调用是立即返回的,因此当发生异常的时候,已经脱离了try..catch..的上下文了,所以异常无法被捕获。

下面来介绍异步编程下几种常用的异常处理方法。

1. callback

通过回调函数可以比较方便地进行异常处理,例如:

function async(callback, errback) {
    setTimeout(function() {
        var rand = Math.random();
        if (rand < 0.5) {
            errback('async error');
        } else {
            callback(rand);
        }
    }, 1000);
}

async(function(result) {
    console.log('scucess:', result);
}, function(err) {
    console.log('fail:', err);
});

这里通过setTimeout来模拟实现了一个异步函数async,该函数可能调用成功,也可能调用失败抛出异常。async接收两个参数,当调用成功时callback会被执行,当抛出异常时errback会被执行。

有时候为了方便,也会将callbackerrback合并为一个回调函数,这也是Node风格回调处理。代码如下:

function async(callback) {
    setTimeout(function() {
        var rand = Math.random();
        if (rand < 0.5) {
            callback('async error');
        } else {
            callback(null, rand);
        }
    }, 1000);
}

async(function(err, result) {
    if (err) {
        console.log('fail:', err);
    } else {
        console.log('success:', result);
    }
});

这里err作为回调函数的第一个参数,如果async调用成功,则err为一个falsy值(可以是nullundefined等,在该例子中使用null);如果async调用失败,则err为抛出的异常。当调用回调函数的时候,先判断err是否为falsy。如果为falsy,则进行异常处理;否则执行成功的回调。

然而在多异步串行的情况下,使用回调函数的方式,就会出现所谓的回调金字塔问题,代码可读性也会大打折扣。例如:

function async(callback) {
    setTimeout(function() {
        var rand = Math.random();
        if (rand < 0.2) {
            callback('async error');
        } else {
            callback(null, rand);
        }
    }, 1000);
}

async(function(err, result) {
    if (err) {
        console.log('fail:', err);
    } else {
        console.log('success:', result);
        async(function(err, result) {
            if (err) {
                console.log('fail:', err);
            } else {
                console.log('success:', result);
                async(function(err, result) {
                    if (err) {
                        console.log('fail:', err);
                    } else {
                        console.log('success:', result);
                    }
                });
            }
        });
    }
});

在这里,回调函数一层嵌一层,而且每一层都要判读是否出错。不仅代码可读性差,维护起来也非常不方便。

2. promise

上面的例子,使用promise的话,代码如下:

function async() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            var rand = Math.random();
            if (rand < 0.5) {
                reject('async error');
            } else {
                resolve(rand);
            }
        }, 1000);
    });
}

async().then(function(result) {
    console.log('success:', result);
}, function(err) {
    console.log('fail:', err);
});

或者使用catch的方式,代码如下:

function async() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            var rand = Math.random();
            if (rand < 0.5) {
                reject('async error');
            } else {
                resolve(rand);
            }
        }, 1000);
    });
}

async().then(function(result) {
        console.log('success:', result);
    })
    .catch(function(err) {
        console.log('fail:', err);
    });

使用promise的好处是执行流程直观,但是理解起来比回调函数要麻烦一些。

对于多异步操作串行的问题,使用promise的方式会使得代码简洁优雅,可读性也很强。代码如下:

function async() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            var rand = Math.random();
            if (rand < 0.2) {
                reject('async error');
            } else {
                resolve(rand);
            }
        }, 1000);
    });
}

function onResolved(result) {
    console.log('success:', result);
}

function onRejected(err) {
    console.log('fail:', err);
}

async().then(onResolved)
    .then(async)
    .then(onResolved)
    .then(async)
    .then(onResolved)
    .catch(onRejected);

3. domain

在Node中,有一个domain模块(在io.js中该模块已经标记为deprecated),可以用来处理异步操作异常。示例代码如下:

var domain = require('domain');

function async(callback) {
    setTimeout(function() {
        var rand = Math.random();
        if (rand < 0.5) {
            throw 'async error';
        } else {
            callback(rand);
        }
    }, 1000);
}

var d = domain.create();
d.on('error', function(err) {
    console.log('fail:', err);
});
d.run(function() {
    async(function(result) {
        console.log('success:', result);
    });
});

Domain类继承自EventEmitter,所以它本质上就是一个事件发生器。