标签: 异步函数

  • 聊聊错误/异常处理

    聊聊错误/异常处理

    又是繁忙的一周。突然发现以前没聊过错误/异常处理,准备分享一下。

    这里我就不细究错误(Error)/异常(Exception)的定义了,大体上程序执行过程中,一旦出现就会阻碍后续代码顺利执行的问题,我们都当它们是错误。我们要保证自己的代码能得到顺利执行,就要防止被它们破坏,就要捕获并处理错误。这就是接下来要讨论的要点。

    有些同学在这个领域搞得不伦不类:要么从来不捕获任何错误;要么所有地方都丢一个 try ... catch ...;要么最外层包一个大 try ... catch ...。这些做法都不对。本文分享我的观点和做法。

    开发时能够不出错,尽量不要出错

    虽然我自己也整天写 bug,并且被自己的 bug 折麽得死去活来,但我还是要说:我们应该在开发阶段尽量避免出现错误,保证我们的代码能够长时间运行不出错。这就需要我们考虑到各种边界情况,提前做出预判,操作前多做检查,避免出错。

    比如,如果使用 node.js 要操作文件,就先判断目标是否存在,目录是否已经创建过。而不是捕获各种可能出现的错误。用户要上传数据给服务器后端处理,就要先尽可能排除掉不可接受的情况,而不是被服务器拒绝后再转达给用户。

    捕获无法在开发阶段排除的错误

    也有一些错误,我们实在无法在开发阶段排除。最常见的就是网络问题。因为我们无法预期用户在怎样的网络条件下使用我们的产品,而现在用户的使用场景又极为丰富:无论是高速运动的火车,还是跨国网络环境,又或是第三方服务突然挂掉,都可能导致网络不畅,而请求失败。

    所以我们必须在发起网络请求时做好错误捕获,并且处理好网络造成的问题。比如:

    1. 告知用户当前状态,正确渲染 UI
    2. 保护好未能保存到服务器上的数据
    3. 提醒用户重试
    4. 提供其它临时保存方案

    简而言之,发起网络请求时没有 try ... catch ... 我在 code review 时是不会通过的。

    开发中见到错误要及时处理

    我见过一些同学的开发环境,跑起来满屏错误,但是好像也能运行,于是他们就不管。这样做是不行的。因为会被抛出的错误大多是未捕获的(Uncaught),它们大概率会破坏到某些代码的执行,没有被影响多半只是因为运气好,没有破坏到当前正在执行的逻辑。早晚还是要遭。

    另外,如果项目遇到了一些奇怪的问题,不知道该怎么解决;同时控制台里有一些奇奇怪怪的报错,那么解决这些报错很可能会带来意想不到的收获。

    总之,不要漏掉错误,不要容忍错误。否则积累技术债。

    要理解错误冒泡的中断机制,尤其是异步操作

    异步回调中的错误处理比较难做,因为回调前后是两个函数栈,所以执行时函数无法捕获到回调时的错误。举个例子吧:

    someAsyncMethod() // 可以直接被外层 try catch 捕获
      .then(() => {
        // 这里如果发生错误,无法被外层捕获
      })
      .catch(e => {
        // 所以我们通常要在最后处理错误
      })

    异步函数由于浏览器的升级,可以在堆栈最底层捕获到上层发生的错误。所以我们可以在底层函数,通常来说也就是执行时的函数捕获错。比如,我们有一个业务函数,要调用一个封装好的请求函数,那么,错误处理就应该放在业务函数里,而不是请求函数里:

    // 业务代码
    async function doDeletePost(id) {
      try {
        await deleteItem('post', id);  
      } catch (e) {
        // 应该在这里处理错误
      }
    }
    
    // 这里虽然是异步函数,请求远程接口,但不需要处理错误
    async function deleteItem(type, id) {
      await fetch(`/api/${type}/${id}, {
        method: 'DELETE',
      });
    }

    另外注意,没有 await 的异步函数就不存在错误捕获,不要写出这样的代码:

    function doDeletePost(id) {
      try {
        deleteItem('post', id); // 没有 await,没有意义
      } catch (e) {
        // 错误处理
      }
    }

    不要封装错误处理,在业务端处理,并提供准确的信息

    有些同学喜欢封装错误处理,我觉得当年的 axios 二次封装难辞其咎。对此我坚决反对,比如:后台同步数据出错,可能不需要告知用户;但是用户主动发起的操作失败就必须告知用户。加载数据失败和删除数据失败,其错误信息也是不一样的,应该准确传达给用户。

    所以,我们应该在业务端发起请求的地方捕获并处理错误,而不是封装一个通用请求类,并且集中处理可能的错误,给出无法区分的信息。

    错误信息要有价值

    有些同学拿到错误之后,直接 alert('出错了。') 这种错误信息对用户、对开发者都没有帮助。以网络请求为例,比较常见的做法是:

    1. 返回的信息首先应该遵守 HTTP 规范,即不同的结果使用不同的响应码。
    2. 返回的响应体包括 code 字段,用来传输错误码,它跟 HTTP 状态码没关系。成功的结果,code=0;错误的结果,code 则使用全局唯一的错误码,方便前后端协同排查。
    3. 错误码可以使用方便理解的规范,比如 XXXX-XXX-X 的形式,从大类到小类再到具体的函数位置等。这样,在任何位置捕获到错误,都可以快速定位到可能出错的环节,进行排查,解决问题。

    总结

    希望大家都掌握好错误处理的方法,能够妥善、快速的解决问题。如果各位对错误处理有一些特殊的理解,或者对错误处理有疑问,欢迎留言讨论。

  • 记一个 `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 在当前栈捕获到错误。