[关闭]
@FunC 2017-05-03T10:01:25.000000Z 字数 7430 阅读 3169

异步编程的异常处理

前端


本文主要参考自 @黄子毅 的 Callback Promise Generator Async-Await 和异常处理的演进, 对内容进行了一定的加工整理,并修改了原文中的一些错误。

回调

1.无法捕获的异常

  1. function fetch(callback) {
  2. setTimeout(() => {
  3. throw Error('请求失败')
  4. })
  5. }
  6. try {
  7. fetch(() => {
  8. console.log('请求处理') // 永远不会执行
  9. })
  10. } catch (error) {
  11. console.log('触发异常', error) // 永远不会执行
  12. }
  13. // 程序崩溃
  14. // Uncaught Error: 请求失败

执行回调时已经不是出于原本的执行栈了,Error发生在下一轮事件循环中,所以没有被try...catch捕获

2.Error-First约定
对于高度依赖异步回调的Node.js,采用了Error-First的约定,即:

  1. callback的第一个参数必须是一个error对象,如果没有出错则该值为null
  2. 第二个参数为异步操作成功后的结果

好处在于,进行异步函数出错时,它不需要知道如何处理Error,也不会直接让程序崩溃。而是把错误交给你,让你自行选择忽略处理或者冒泡给别的callback

Promise

前置知识:promise内部的错误不会冒泡到全局
1.reject的Error都能捕获(实质为microtask中抛出的异常)

  1. //以下异常都能被捕获
  2. var prom = new Promise((resolve,reject) => {
  3. throw Error('noo'); // 直接throw Error可捕获(因为在microtask中)
  4. // reject('nooo'); // reject当然也可以
  5. })
  6. prom
  7. .then((err) => console.log('working',err)) // 不运行
  8. .catch((e) => console.log(e)); //运行
  9. //又如
  10. var prom = new Promise((resolve,reject) => {
  11. resolve('resolved')
  12. })
  13. prom
  14. .then(() => {
  15. throw Error('nooo');//也是在microtask中抛出,可捕获
  16. // return Promise.reject('nooo') // 返回一个rejected的promise也能捕获
  17. })
  18. .then((err) => console.log('working',err))// 不运行
  19. .catch((e) => console.log(e));// 运行

注意链式调用中,能传递的错误只有在microtask中throw Error以及return rejected的promise,其它返回值均包装成resolve的promise,包括Error:

  1. var prom = new Promise((resolve,reject) => {
  2. resolve('resolved')
  3. })
  4. prom
  5. .then(() => {
  6. return Error('nooo') // 不视作异常, 返回一个Error对象
  7. //Promise.reject('nooo') // 不视作异常,因为返回值为undefined
  8. })
  9. .then((err) => console.log('working',err)) // 运行
  10. .catch((e) => console.log(e)); // 不运行

2.macrotask中抛出的异常无法捕获

  1. var prom = new Promise((resolve,reject) => {
  2. setTimeout(() => {
  3. throw Error('nooo');
  4. },0)
  5. })
  6. prom
  7. .then((err) => console.log('working',err))
  8. .catch((e) => console.log(e));
  9. //程序崩溃

其实很好理解,setTimeout中的回调是进入macrotask的,即进入新一轮事件循环,此时上下文环境已改变,即相当于单独使用:

  1. () => {throw Error('nooo');

另外prom实质上处于pending状态,自然也无法进入then的链式调用。

解决方案也很简单,可以使用try...catch捕获错误,然后传递给reject。

  1. var prom = new Promise((resolve,reject) => {
  2. setTimeout(() => {
  3. try{
  4. throw Error('nooo');
  5. }catch(e){
  6. reject(e) // 捕获异常,并传递给catch
  7. }
  8. },0)
  9. })
  10. prom
  11. .then((err) => console.log('working',err))
  12. .catch((e) => console.log(e));

当然,一个很常见的场景就是使用第三方函数,然后第三方函数在macrotask中抛出了错误

  1. function thirdFunction() {
  2. setTimeout(() => {
  3. throw Error('就是任性')
  4. })
  5. }
  6. Promise.resolve(true).then(() => {
  7. thirdFunction()
  8. }).catch(error => {
  9. console.log('捕获异常', error)
  10. })
  11. // 程序崩溃
  12. // Uncaught Error: 就是任性

很遗憾,只能让其改为promise并进行reject(可见promise是未来大势所趋)

  1. function thirdFunction() {
  2. return new Promise((resolve, reject) => {
  3. setTimeout(() => {
  4. reject('收敛一些')
  5. })
  6. })
  7. }
  8. Promise.resolve(true).then(() => {
  9. // 注意这里的return,缺少return会怎样?请认真阅读前文
  10. // 然而此处的return极其容易忘记,更优雅的方案见后文async
  11. return thirdFunction()
  12. }).catch(error => {
  13. console.log('捕获异常', error) // 捕获异常 收敛一些
  14. })

3.利用macrotask抛出的异常
尽管最后又.catch()捕获错误,但万一.catch()内部抛出错误,该错误就无法被捕捉到了(promise内部的错误不会冒泡到全局)
这时利用macrotask抛出的异常能冒泡到全局这一特性,实现.done()方法

  1. Promise.prototype.done = function() {
  2. this.catch((err) => {
  3. setTimeout(() => {throw err}, 0)
  4. });
  5. }
  6. //用法
  7. asyncFunc()
  8. .then(f1)
  9. .catch(r1)
  10. .then(f2)
  11. .done();

Promise异常处理小结:
1. macrotask中抛出的错误(throw Error('msg'))无法捕获,需通过promise的reject来传递错误
2. microtask中的错误,直接throw Error('msg')returnreject的promise都能传递
3. then中除了returnreject的promise,其它返回值都会被包装成resolve的promise

Async函数

async函数就是自动运行的状态机,而状态机(Generator)又提供切出切入(并保留上下文)的功能(有没有想起前文因上下文丢失而无法处理的Error?)

Async的异常

await不会自动捕获异常,但会中断async函数的进行,而不是直接让程序崩溃。

  1. function fetch(callback) {
  2. return new Promise((resolve, reject) => {
  3. reject()
  4. })
  5. }
  6. async function main() {
  7. const result = await fetch() // rejected的promise被传入
  8. console.log('请求处理', result) // 不会执行,async函数终止
  9. }
  10. main()

Async捕获异常

Async函数能通过使用try...catch捕获异常(当然在macrotask中抛出的异常仍旧无法捕获,原因同promise)

  1. function thirdFunction() {
  2. return new Promise((resolve, reject) => {
  3. setTimeout(() => {
  4. reject('收敛一些')
  5. })
  6. })
  7. }
  8. async function main() {
  9. try {
  10. const result1 = await secondFunction() // 如果不抛出异常,后续继续执行
  11. const result2 = await thirdFunction() // 抛出异常
  12. const result3 = await thirdFunction() // 永远不会执行
  13. console.log('请求处理', result) // 永远不会执行
  14. } catch (error) {
  15. console.log('异常', error) // 异常 收敛一些
  16. }
  17. }
  18. main()

值得注意的是,async返回的是一个promise对象,所以也可以通过对Async返回的promise对象进行异常捕获:

  1. function thirdFunction() {
  2. return new Promise((resolve, reject) => {
  3. setTimeout(() => {
  4. reject('收敛一些')
  5. })
  6. })
  7. }
  8. async function main() {
  9. const result1 = await Promise.resolve(true) // 如果不抛出异常,后续继续执行
  10. const result2 = await thirdFunction() // 抛出异常
  11. const result3 = await thirdFunction() // 永远不会执行
  12. console.log('请求处理', result1) // 永远不会执行
  13. }
  14. main()
  15. .catch((err) => console.log('异常',err)); // 异常 收敛一点

模仿async

async/await不在ES6标准中,但其实我们也可以对generator进行包装,让其自动运行:

  1. function autoRun(generator) {
  2. //首先async返回的是一个promise
  3. return new Promise((resolve, reject) => {
  4. var gen = generator();
  5. function nextThrow(func) {
  6. // 当上一个yielded的value是rejection时
  7. // 尝试向generator内部抛出错误
  8. try{
  9. var next = func();
  10. }catch(e){
  11. // 如果generator内部没有try...catch捕获错误,则会在此处捕获
  12. // 并让autoRun返回一个rejection,与async行为一致
  13. reject(e)
  14. }
  15. if(next.done){
  16. resolve(next.value);
  17. }else{
  18. // 保证rejection和非promise值能被处理
  19. Promise.resolve(next.value).then(
  20. (result) => nextThrow(() => gen.next(result)),
  21. (rejection) => nextThrow(() => gen.throw(rejection))// 值为rejection时向generator内部抛出错误,在下一轮才执行是为了方便捕获
  22. )
  23. }
  24. }
  25. //启动
  26. nextThrow(() => gen.next());
  27. })
  28. }
  29. // 使用
  30. function* gen() {...}
  31. var resultProm = autoRun(gen);
  32. result.then(...)
  33. .catch(...)//能像async一样使用

冒泡处理异常

考虑一下代码:

  1. const successRequest = () => Promise.resolve('a')
  2. const failRequest = () => Promise.reject('b')
  3. class Action {
  4. async successReuqest() {
  5. const result = await successRequest()
  6. console.log('successReuqest', '处理返回值', result) // successReuqest 处理返回值 a
  7. }
  8. async failReuqest() {
  9. const result = await failRequest()
  10. console.log('failReuqest', '处理返回值', result) // 永远不会执行
  11. }
  12. async allReuqest() {
  13. const result1 = await successRequest()
  14. console.log('allReuqest', '处理返回值 success', result1) // allReuqest 处理返回值 success a
  15. const result2 = await failRequest()
  16. console.log('allReuqest', '处理返回值 success', result2) // 永远不会执行
  17. }
  18. }
  19. const action = new Action()
  20. action.successReuqest()
  21. action.failReuqest()
  22. action.allReuqest()

程序不会崩溃,因为promise内部的异常不冒泡到全局(即使throw Error也是,但如果macrotask上throw Error即使try...catch也无法捕获)

为了捕获异常,需要给每个async都包裹一层try...catch或者给async的返回结果加上.catch(),难免显得有点繁琐。于是我们的解决方案登场了:

Decorator(修饰器)

这是ES7的一个提案,目前Babel已通过插件支持。
简单来说就是修饰器能在代码编译阶段改变类的行为。
下面是一个简单的类修饰器示例

  1. function testable(target) {
  2. target.isTestable = true;
  3. }
  4. @testable
  5. class MyTestableClass {}
  6. console.log(MyTestableClass.isTestable) // true

类修饰器接受一个参数target,为类的构造函数。现在,我们想给类中每一个async函数都包裹一层try...catch

  1. //我们先对类装饰器进行柯里化,传入一个errorHandler作参数
  2. const asyncErrorWrapper = (errorHandler) => (target) => {
  3. const props = Object.getOwnPropertyNames(target.prototype);
  4. props.forEach((prop) => {
  5. var value = target.prototype[prop];
  6. // 判断如果是async函数则包裹try...catch
  7. if(Object.prototype.toString.call(value) === '[object AsyncFunction]'){
  8. target.prototype[prop] = async function (...args) {
  9. try{
  10. // 注意千万不能漏掉await
  11. await value.apply(this,args);
  12. }catch(err){
  13. errorHandler(err);
  14. }
  15. }
  16. }
  17. });
  18. }
  19. // 生成一个errorHandler为console.log的装饰器
  20. const logError = asyncErrorWrapper((err) => console.log('异常',err));
  21. const successRequest = () => Promise.resolve('a')
  22. const failRequest = () => Promise.reject('b')
  23. //应用装饰器
  24. @logError
  25. class Action {
  26. async successReuqest() {
  27. const result = await successRequest();
  28. console.log('successReuqest', '处理返回值', result);
  29. }
  30. async failReuqest() {
  31. const result = await failRequest();
  32. console.log('failReuqest', '处理返回值', result);
  33. }
  34. async allReuqest() {
  35. const result1 = await successRequest();
  36. console.log('allReuqest', '处理返回值 success', result1);
  37. const result2 = await failRequest();
  38. console.log('allReuqest', '处理返回值 success', result2);
  39. }
  40. }
  41. const action = new Action();
  42. action.successReuqest()
  43. action.failReuqest()
  44. action.allReuqest()

通过这样的统一处理,前端可以用alert兜底,node端也可以返回500兜底,避免程序崩溃的同时也掌握了处理异常的主动权。

One more thing

别忘了可能还有漏网之鱼,我们可以通过监听全局错误来处理:
浏览器端,监听未处理的rejection

  1. // 仅chrome49支持
  2. window.addEventListener("unhandledrejection", function (event) {
  3. console.warn("WARNING: Unhandled promise rejection. Shame on you! Reason: "+ event.reason);
  4. });

Node.js端(v1.4.1已支持)

  1. // 用Map记录没处理的rejection
  2. const unhandledRejections = new Map();
  3. process.on('unhandledRejection', (reason, p) => {
  4. unhandledRejections.set(p, reason);
  5. });
  6. // 处理后从Map中删除记录
  7. process.on('rejectionHandled', (p) => {
  8. unhandledRejections.delete(p);
  9. });
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注