likes
comments
collection
share

关于JavaScript异常捕获的几点建议

作者站长头像
站长
· 阅读数 10

前言

为了解决JavaScript回调地狱的问题,ES6+引入了Promise,通过将返回值代理到Promise中,并结合pending, rejected, fulfilled三种状态的转化返回Promise,将异步的返回值存储在未来某一时刻可以用到的promise中,方便解析。

Promise通过then()链的方式调用,发生错误时通过rejected promise传递错误,可以像try...catch一样捕获和处理异常,但是使用不当可能导致意料之外的行为,本文列举了一些常见的使用错误,在使用中应当注意。

1. .then链式调用异步,捕获不到异常

👉🏻现象:在.then()链中调用异步函数,某些调用方式不同会导致捕获不到异步抛出的异常

异步函数异常捕获demo-sandbox

const before = () => {
    const p = new Promise((resolve, reject) => {
        // resolve('before')
        console.log('ok');
        reject(new Error('error!!!'));
    });
    return p;
};
const handleClick = () => {
    const p = new Promise((resolve, reject) => {
        resolve(true);
    });
    p
        // ❌ catch不到
        .then(() => {
            before(); 
        })
        // .then(before)
        // .then(async () => {
        //   await before();
        // })
        .then(() => {
            console.log('then');
        })
        .catch(e => {
            console.error(e);
        });
};

💡原因

缺少return语句:before()没有return,返回undefined,无错误抛出,后续的then链会继续执行,但是因为错误没有被捕获/正确处理,会导致页面或应用程序崩溃。

❓疑惑

rejected promise不就是异常吗,那为什么没有触发呢?

⭐️ The return is implicit under some circumstances

实际上rejected promise并不是异常,如果不被return出去,就不会触发error机制(类似的,如果创建一个新的错误对象,但是没有throw,那么同样也不会触发异常)

实际上,rejected promise并不是异常,如果没有return出去,那么就不会触发error

2. try...catch无法捕获异步函数中通过return返回的rejected error.

👉🏻需要使用await接收异步函数的返回,再将结果return出去

const a = async () => {
    return new Promise((resolve, reject) => {
        reject('error')
    })
};
const b = async () => {
    try {
        // ❌捕获不到
        // return a();
        const res = await a();
        return res;
    }
    catch (e) {
        console.log('catch exception!!!')
    }
}
b();

3. 在Promise的构造函数中使用async会导致异常捕获不到

let p = new Promise(resolve => {
  ""(); // TypeError
  resolve();
});
(async () => {
  await p;
})().catch(e => console.log("Caught: " + e)); // Catches it.
let p = new Promise(async resolve => {
  ""(); // TypeError
  resolve();
});
(async () => {
  await p;
})().catch(e => console.log("Caught: " + e)); // Doesn't catch it!

💡第一段代码能够捕获到异常,因为异常由Promise的构造函数直接reject出去;第二段代码不能,因为async关键字会隐式生成一个新的promise,由async函数自身处理。

4.🪜JavaScript Reject V.S. Throw

都是用于抛出异常,reject和throw有什么区别,适用于什么场景

先简单对比一下:

  • 都可用于触发错误机制
  • reject是promise的一部分,结合promise使用;throw不受限制
  • throw之后的语句都不会执行了,reject之后的语句会正常执行,但如果reject前边有return,那么其后的语句同样也不会继续执行

但实际上,这俩并没有优劣之分,但是在某种场景下throw是无效的。在promise的回调中throw可以正常使用,一旦出现异步,throw就无效了。

比如:

new Promise(function() {
  setTimeout(function() {
    throw 'or nah';
    // return Promise.reject('or nah'); also won't work
  }, 1000);
}).catch(function(e) {
  console.log(e); // doesn't happen
});

正确捕获的方式:

new Promise((resolve, reject) => {
  setTimeout(function() {
    reject('or nah');
  }, 1000);
}).catch(function(e) {
  console.log(e); // works
});

5. 异常的正确处理方式

异常需要被正确捕获。

  • 嵌套的try...catch

    如果代码中有嵌套的try...catch,那么异常会被内部的catch捕获:内层异常被处理,那么外层就捕获不到了;内层直接throw,那么内层try...catch无意义

  • .then链的异常捕获,需要保证异常在UI层被捕获。

    不要过早的吞掉异常,需要保证catch在最终调用处调用,避免异常暴露导致程序或页面崩溃。

小结

  1. rejected promise和throw都可用于触发异常,没有优劣之分;

  2. promise回调中存在异步,那么rejected promise必要要return出去才可被后续处理,否则返回undefined,即使存在异常也不会被处理,会导致页面或应用程序崩溃;

  3. promise回调中的异步函数,执行结果需要被return出去异常才会被捕获:要么直接作为回调参数,要么使用async...await执行,要么使用return

  4. 捕获promise回调中异步函数的异常,throw是无效的,创建一个新的rejected promise也是无效的,需要使用当前promise的rejecte函数

  5. 在promise的构造函数中使用async关键字会导致异常捕获不到,因为async关键字会隐式生成一个新的promise,由async函数自身处理

参考

Do I need return after early resolve/reject?

stackoverflow-anti-pattern

try...catch

嵌套的try...catch