本文主要参考自 @黄子毅 的 Callback Promise Generator Async-Await 和异常处理的演进, 对内容进行了一定的加工整理,并修改了原文中的一些错误。
回调
1.无法捕获的异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function fetch(callback) { setTimeout(() => { throw Error('请求失败') }) } try { fetch(() => { console.log('请求处理') }) } catch (error) { console.log('触发异常', error) }
|
执行回调时已经不是出于原本的执行栈了,Error发生在下一轮事件循环中,所以没有被try...catch
捕获
2.Error-First
约定
对于高度依赖异步回调的Node.js,采用了Error-First的约定,即:
- callback的第一个参数必须是一个error对象,如果没有出错则该值为null
- 第二个参数为异步操作成功后的结果
好处在于,进行异步函数出错时,它不需要知道如何处理Error,也不会直接让程序崩溃。而是把错误交给你,让你自行选择忽略、处理或者冒泡给别的callback。
Promise
前置知识:promise内部的错误不会冒泡到全局
1.reject的Error都能捕获(实质为microtask中抛出的异常)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| var prom = new Promise((resolve,reject) => { throw Error('noo'); }) prom .then((err) => console.log('working',err)) .catch((e) => console.log(e)); var prom = new Promise((resolve,reject) => { resolve('resolved') }) prom .then(() => { throw Error('nooo'); }) .then((err) => console.log('working',err)) .catch((e) => console.log(e));
|
注意链式调用中,能传递的错误只有在microtask中throw Error
以及return
rejected的promise,其它返回值均包装成resolve的promise,包括Error:
1 2 3 4 5 6 7 8 9 10 11
| var prom = new Promise((resolve,reject) => { resolve('resolved') }) prom .then(() => { return Error('nooo') }) .then((err) => console.log('working',err)) .catch((e) => console.log(e));
|
2.macrotask中抛出的异常无法捕获
1 2 3 4 5 6 7 8 9 10 11
| var prom = new Promise((resolve,reject) => { setTimeout(() => { throw Error('nooo'); },0) }) prom .then((err) => console.log('working',err)) .catch((e) => console.log(e));
|
其实很好理解,setTimeout中的回调是进入macrotask的,即进入新一轮事件循环,此时上下文环境已改变,即相当于单独使用:
1
| () => {throw Error('nooo');
|
另外prom实质上处于pending状态,自然也无法进入then的链式调用。
解决方案也很简单,可以使用try…catch捕获错误,然后传递给reject。
1 2 3 4 5 6 7 8 9 10 11 12 13
| var prom = new Promise((resolve,reject) => { setTimeout(() => { try{ throw Error('nooo'); }catch(e){ reject(e) } },0) }) prom .then((err) => console.log('working',err)) .catch((e) => console.log(e));
|
当然,一个很常见的场景就是使用第三方函数,然后第三方函数在macrotask中抛出了错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function thirdFunction() { setTimeout(() => { throw Error('就是任性') }) } Promise.resolve(true).then(() => { thirdFunction() }).catch(error => { console.log('捕获异常', error) })
|
很遗憾,只能让其改为promise并进行reject(可见promise是未来大势所趋)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function thirdFunction() { return new Promise((resolve, reject) => { setTimeout(() => { reject('收敛一些') }) }) } Promise.resolve(true).then(() => { return thirdFunction() }).catch(error => { console.log('捕获异常', error) })
|
3.利用macrotask抛出的异常
尽管最后又.catch()
捕获错误,但万一.catch()
内部抛出错误,该错误就无法被捕捉到了(promise内部的错误不会冒泡到全局)
这时利用macrotask抛出的异常能冒泡到全局这一特性,实现.done()
方法
1 2 3 4 5 6 7 8 9 10 11
| Promise.prototype.done = function() { this.catch((err) => { setTimeout(() => {throw err}, 0) }); } asyncFunc() .then(f1) .catch(r1) .then(f2) .done();
|
Promise异常处理小结:
- macrotask中抛出的错误(
throw Error('msg')
)无法捕获,需通过promise的reject来传递错误
- microtask中的错误,直接
throw Error('msg')
和return
reject的promise都能传递
then
中除了return
reject的promise,其它返回值都会被包装成resolve的promise
Async函数
async函数就是自动运行的状态机,而状态机(Generator)又提供切出切入(并保留上下文)的功能(有没有想起前文因上下文丢失而无法处理的Error?)
Async的异常
await不会自动捕获异常,但会中断async函数的进行,而不是直接让程序崩溃。
1 2 3 4 5 6 7 8 9 10 11 12
| function fetch(callback) { return new Promise((resolve, reject) => { reject() }) } async function main() { const result = await fetch() console.log('请求处理', result) } main()
|
Async捕获异常
Async函数能通过使用try...catch
捕获异常(当然在macrotask中抛出的异常仍旧无法捕获,原因同promise)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function thirdFunction() { return new Promise((resolve, reject) => { setTimeout(() => { reject('收敛一些') }) }) } async function main() { try { const result1 = await secondFunction() const result2 = await thirdFunction() const result3 = await thirdFunction() console.log('请求处理', result) } catch (error) { console.log('异常', error) } } main()
|
值得注意的是,async返回的是一个promise对象,所以也可以通过对Async返回的promise对象进行异常捕获:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function thirdFunction() { return new Promise((resolve, reject) => { setTimeout(() => { reject('收敛一些') }) }) } async function main() { const result1 = await Promise.resolve(true) const result2 = await thirdFunction() const result3 = await thirdFunction() console.log('请求处理', result1) } main() .catch((err) => console.log('异常',err));
|
模仿async
async/await不在ES6标准中,但其实我们也可以对generator进行包装,让其自动运行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| function autoRun(generator) { return new Promise((resolve, reject) => { var gen = generator(); function nextThrow(func) { try{ var next = func(); }catch(e){ reject(e) } if(next.done){ resolve(next.value); }else{ Promise.resolve(next.value).then( (result) => nextThrow(() => gen.next(result)), (rejection) => nextThrow(() => gen.throw(rejection)) ) } } nextThrow(() => gen.next()); }) } function* gen() {...} var resultProm = autoRun(gen); result.then(...) .catch(...)
|
冒泡处理异常
考虑一下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| const successRequest = () => Promise.resolve('a') const failRequest = () => Promise.reject('b') class Action { async successReuqest() { const result = await successRequest() console.log('successReuqest', '处理返回值', result) } async failReuqest() { const result = await failRequest() console.log('failReuqest', '处理返回值', result) } async allReuqest() { const result1 = await successRequest() console.log('allReuqest', '处理返回值 success', result1) const result2 = await failRequest() console.log('allReuqest', '处理返回值 success', result2) } } const action = new Action() action.successReuqest() action.failReuqest() action.allReuqest()
|
程序不会崩溃,因为promise内部的异常不冒泡到全局(即使throw Error也是,但如果macrotask上throw Error即使try…catch也无法捕获)
为了捕获异常,需要给每个async都包裹一层try...catch
或者给async的返回结果加上.catch()
,难免显得有点繁琐。于是我们的解决方案登场了:
Decorator(修饰器)
这是ES7的一个提案,目前Babel已通过插件支持。
简单来说就是修饰器能在代码编译阶段改变类的行为。
下面是一个简单的类修饰器示例
1 2 3 4 5 6 7 8
| function testable(target) { target.isTestable = true; } @testable class MyTestableClass {} console.log(MyTestableClass.isTestable)
|
类修饰器接受一个参数target
,为类的构造函数。现在,我们想给类中每一个async函数都包裹一层try...catch
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| const asyncErrorWrapper = (errorHandler) => (target) => { const props = Object.getOwnPropertyNames(target.prototype); props.forEach((prop) => { var value = target.prototype[prop]; if(Object.prototype.toString.call(value) === '[object AsyncFunction]'){ target.prototype[prop] = async function (...args) { try{ await value.apply(this,args); }catch(err){ errorHandler(err); } } } }); } const logError = asyncErrorWrapper((err) => console.log('异常',err)); const successRequest = () => Promise.resolve('a') const failRequest = () => Promise.reject('b') @logError class Action { async successReuqest() { const result = await successRequest(); console.log('successReuqest', '处理返回值', result); } async failReuqest() { const result = await failRequest(); console.log('failReuqest', '处理返回值', result); } async allReuqest() { const result1 = await successRequest(); console.log('allReuqest', '处理返回值 success', result1); const result2 = await failRequest(); console.log('allReuqest', '处理返回值 success', result2); } } const action = new Action(); action.successReuqest() action.failReuqest() action.allReuqest()
|
通过这样的统一处理,前端可以用alert兜底,node端也可以返回500兜底,避免程序崩溃的同时也掌握了处理异常的主动权。
One more thing
别忘了可能还有漏网之鱼,我们可以通过监听全局错误来处理:
在浏览器端,监听未处理的rejection
1 2 3 4
| window.addEventListener("unhandledrejection", function (event) { console.warn("WARNING: Unhandled promise rejection. Shame on you! Reason: "+ event.reason); });
|
在Node.js端(v1.4.1已支持)
1 2 3 4 5 6 7 8 9
| const unhandledRejections = new Map(); process.on('unhandledRejection', (reason, p) => { unhandledRejections.set(p, reason); }); process.on('rejectionHandled', (p) => { unhandledRejections.delete(p); });
|