标签: async function

  • 函数,栈,try…catch,以及异步

    函数,栈,try…catch,以及异步

    前两天《记一个 `try…catch` 异步函数的坑》发出后,有同学表示对最后一句不解:

    异步函数的调用可能并不在当前栈,也无法被 try…catch 在当前栈捕获到错误。

    要解释清楚,一两行是不够的,于是写这篇文章展开解释一下。(这篇文章是基础向。)

    函数与栈

    首先来看这样一段代码:

    function a() {
      // ...
    }
    
    function b() {
      // 代码1 
      a();
      // 代码2
    }
    
    function c() {
      // 代码3
      b()
      // 代码4
    }
    
    c();

    它会怎么执行呢?简单来说是这样:

    1. 构建一个栈
    2. c 推入栈,开始执行 代码3
    3. b 推入栈,开始执行 代码1
    4. 将 a 推入栈,开始执行
    5. a 执行完,出栈
    6. 开始执行 代码 2
    7. b 执行完,出栈
    8. 开始执行 代码 4
    9. c 执行完,出栈

    栈是一个先入后出的数据结构,你可以把它当成一个桶,先放进去的东西会被后放进去的东西压在下面,需要先把上层的东西,也就是后放进去的东西拿出来才能拿到先放进去的东西。

    发生错误

    如果代码执行中发生错误,就会在错误处中断,并逐个出栈。此时我们不仅能看到错误本身,还能看到错误发生处的完整栈信息,对 debug 很有帮助。

    try...catch

    异步函数出现之前,try...catch 只能捕获当前栈的错误。比如,同样是上面那段代码,稍微改动一下:

    // 之前定义 a b c 的代码不变
    
    function d() {
      throw new Error('oh my god');
    }
    
    try {
      c();
    } catch (e) {
      console.error(e);
    }
    
    d();

    这段代码里,c() 执行时如果有错误,可以被 try...catch 捕获到;但是 d() 位于另一个栈(或同一个栈的另一个堆叠),就无法捕获。

    异步 callback & Promise

    Promise 成为规范被纳入标准之前,我们处理异步操作时需要使用回调(callback)。比如侦听事件:

    try {
      $('.btn').on('click', function (e) {
        console.log('hello');
      });
    } catch (e) {
      console.error(e);
    }

    请注意:这里添加侦听函数的代码,也就是 $().on() 的部分,是在当前栈执行的;而用户点击后执行的操作,即 console.log('hello') 的部分,是在将来某个事件调用栈里执行的。所以,如果后面的函数里有问题,那么如上面所示的代码是无法捕获到错误的。

    Promise 也一样。比如下面这段代码:

    new Promise((resolve) => {
      // 执行完后回调
      func0(resolve);
    })
      .then(() => {
        return func1();
      })
      .then(() => {
        return func2();
      });

    这里的 func0func1func2 三个函数都是在不同调用栈里执行的,所以如果你在最外面 try...catch,无法捕获到错误。

    这样会给我们编写代码带来一些困难。比如,有时候我们需要同时发起好几个网络请求,有些会成功有些会失败。我们并不知道每个失败请求对应的构造函数是怎么执行的,只能依靠请求内容进行判断,就比较麻烦。

    这个问题,在异步函数中得到了解决。

    异步函数 async function

    这个变化主要是 await 带来的。

    前篇文章所说,在不使用 await 的情况下,try...catch 只会捕获当前调用栈的错误。对于异步函数来说,它在当前栈会返回 Promise 实例,然后就顺利结束,不会抛出任何错误。所以 try...catch 也无法捕获任何错误。

    但是添加 await 之后,情况就不同了。这个时候,try...catch 会捕获整个 Promise 里所有的错误,即使函数位于不同调用栈,也可以正确捕获并且显示在一起,方便调试。

  • 记一个 `try…catch` 异步函数的坑

    记一个 `try…catch` 异步函数的坑

    前几天遇到一个问题:想捕获异步函数的错误,但是捕获不到。我的代码大概是这样:

    try {
      (async () => {
        // 一大堆异步函数
      })();
    } catch (e) {
      if (e.message.startsWith('my error:') {
        // 我的错误,提示一下即可
      } else {
        // 某种不知名错误,继续抛出
        throw new Error(e.message);
      }
    }

    不知道读者是否发现问题,我当时是左看右看没看出来。后来只好写最小用例缩小问题范围,终于找到问题所在:异步函数前面缺少 await。于是想通了,上面的代码其实被引擎解释成:

    1. try 一个函数
    2. 这个函数的返回值是一个 Promise
    3. Promise 是正常对象,所以 try 成功
    4. 至于 Promise 里的函数是否失败,不关 try 的事,所以自然捕获不到

    所以正确的写法是:

    // 外面先套一层,不然不能用 await
    (async () => {
      try {
        // 再加上 `await`
        await (async () => {
          // 异步函数体
        })();
      } catch (e) {
        // ....
      }
    })();

    这个 await 至关重要。这是异步函数的一大特性,即 try...catch 可以捕获整个异步操作前后所有栈的异常,而不仅仅是当前函数的异常。同时我们也要注意,异步函数的调用可能并不在当前栈,也无法被 try...catch 在当前栈捕获到错误。

  • JavaScript 异步开发全攻略

    JavaScript 异步开发全攻略

    之前在 GitChat 做过一次分享:《JavaScript 异步开发全攻略》。在我看来,原始内容可能不够完美,但通过后来的维护,可以把它打磨得越来越好。昨天在 SF 上回答了一个 Vuex 的 action 里使用 Promise 的问题,然后就想去补充一下这方面的内容。结果发现 GitChat 竟然不支持编辑文章,只能把内容发给运营人工修改。

    索性把内容放到 Gitbook 上好了,反正 GitChat SEO 也不做,文章也自然移动到第二页了。试了试,没有被墙,很好。于是简单整理了一下,上传。

    欢迎阅读,欢迎分享,因为我会不时更新新内容,请关注 star:

    JavaScript 异步开发全攻略