标签: 错误处理

  • 聊聊错误/异常处理

    聊聊错误/异常处理

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

    这里我就不细究错误(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 的形式,从大类到小类再到具体的函数位置等。这样,在任何位置捕获到错误,都可以快速定位到可能出错的环节,进行排查,解决问题。

    总结

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

  • 在 Code.fun 做 Code Review(三):聊聊 Promise 的错误处理、如何真正学到技术

    在 Code.fun 做 Code Review(三):聊聊 Promise 的错误处理、如何真正学到技术

    嗯,不知不知觉这个系列写到第三篇,这一篇会改变一下写法,从一次 Code Review 出发,讲解几个技术点,然后分享一下技术学习的经验,以及 Code Review 的用途。希望对大家有帮助。

    顺便推广下前两篇:

    0. 起因

    某天,我给一位同事做 code review,看到下面这段代码:

    const err = new Error('错误信息');
    
    return Promise.reject(err);

    于是我就回复:error 最好直接 throw

    然后他回复我:如果改成 throw 的话,这个 catch 好像是捕获不到 throw 的。

    这就很诡异了,这不符合 Promise 的设计;而 Promise 不是新生事物,它有很大的测试集可以保证行为符合预期,我觉得我们想遇到问题都很困难。于是我就跟这位同事连线,帮他分析问题。

    1. 真正的问题

    连线之后,我发现他的真正问题并不是按照我的要求修改代码之后遇到的。由于基础不太牢靠,他先写了一段实验代码,想要验证我的要求,结果这段代码行为出现异常:

    他希望能够通过 .catch(err => console.log(err)) 捕获并记录错误,但是从控制台的输出来看,错误却是 Uncaught(未捕获)。那么问题出在哪儿呢?

    作为曾经讲解过 Promise 的我当然是一眼就看出来问题所在,但是这位同学却琢磨不透。于是我就把问题发到研发群里,果然又问倒好几位同学。

    现在我请问各位读者老爷,你们知道么?或者换个方向,如果想正常使用 .catch() 捕获到错误,应该怎么修改呢?

    2. 问题解答

    Promise

    这里我们必须回到 Promise 的规范,才能了解上面截图的问题所在(关于详细规范,请参阅 MDN Promise,这里只摘我们需要的部分):

    1. Promise 主要用来改进编写异步函数的体验。
    2. 通过 new Promise(function (resolve, reject) {}) 会创建一个 Promise 实例,其中的参数应该是一个函数(通常是异步函数),接受两个参数:resolvereject。当异步操作成功之后,应调用 resolve(result) 并传递结果;当异步操作失败,应调用 reject(reason) 并传递错误信息。
    3. Promise 有三个状态:pendingfulfilledrejected,起始状态是 pending,变更后,就固定下来,不会再次变更。
    4. 如果异步函数本身抛出错误,Promise 也会进入 rejected 状态。
    5. fulfilled 的 Promise 实例会转入 .then() 处理;rejected 会转入 .catch() 处理。

    我们再回头看上面的截图,这里的问题在于,错误是在 setTimeout(异步函数)的回调函数里抛出的,抛出时当前 Promise 所在的执行栈已经结束了,回调函数是全新开启的执行栈,所以 Promise 无法捕获到它里面的异常;而它也没有主动调用 reject(err) 传出错误,所以就变成了 Uncaught(未捕获)。

    函数执行栈

    既然说到执行栈,我们就顺便补充一下执行栈的知识吧。形如以下代码:

    function a() {
      b();
    }
    function b() {
      c();
    }
    function c() {
      d();
    }
    function d() {
      // do something
    }
    a();

    当函数 a 执行的时候,运行时会开启一个新的执行栈,并且把 a 入栈接下来发现 a 调用了 b,就又会把 b 入栈;然后发现 c……运行时会重复这个过程。当一个函数执行完毕,比如 d,运行时就会把它移出栈,然后继续执行 c 的剩下部分。

    对于 JavaScript 而言,错误是可以冒泡的。即在 d 抛出的异常,如果没有被捕获,它就会一直上浮,直到回到全局。所以我们可以在栈内的任何一个环节捕获它;但如果跨栈,那就无法捕获。

    Event Loop

    异步函数的回调函数会由 Event Loop 开启新栈执行,与所以异步函数自身处于不同的执行栈,所以错误不会被异步函数捕获,自然也不会改变 Promise 的状态。

    解决问题

    所以,要正确捕获错误的话,就需要想办法捕获异步函数的回调函数的错误,即:

    function yy() {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          // 方案1
          reject(new Error('错误信息'));
          // 方案2,实际中很少这么写,这里只是用来演示。但要注意,捕获必须放在回调栈,才能捕获到发生的错误。
          try {
            throw new Error('错误信息');
          } catch (e) {
            reject(e);
          }
        }, 1000);
    
        // 实际场景中,更多是这样的,err 作为参数传给回调函数
        fs.writeFile(path, content, 'utf8', (err) => {
          if (err) {
            return reject(err);
          }
          resolve();
        });
      });
    }

    3. 如何学习技术

    对于上述问题,我们只要掌握 Promise 规范、函数执行栈、Event Loop,就很容易判断。但是如果这三个概念有一个不太清楚,就容易犯迷糊。所以,很多时候,要避免出问题、要保证软件正常工作,我们必须清楚了解每个技术的规范、定义。

    比如 Promise、原型链、闭包等等,它们都不是自然界的产物;而是在长期软件开发实践中,由开发者总结设计出来,用于解决特定问题的发明。他们都有严格的定义、功用、优缺点,等等。我们日常开发,应该把这些东西当成知识储备起来,针对需求做排列组合,给出解决方案。

    有些同学相反,他们会记下来一些技术的用法,然后反推这些技术的特性。如果对一个技术不熟悉,他们会尝试做类似实际场景的实验,然后再想办法搬到实际场景中。这样做,覆盖常见场景可能没问题,遇到陌生的领域就容易踩坑。

    所以,我要强调,对于新技术,大家不熟悉又要尽快用在实际开发中,临时做点实验记住些经验用法当然可以。但不应满足于此,要找时间把技术规范补起来,把完整的设计至少读个几遍。一方面纠正自己的错误实践,另一方面,也可以扩大你对这个技术的应用面。

    4. Code Review 的作用

    这里也不得不提 Code Review 的一个重要作用:

    传承知识。Code Reviewe 是非常好的查缺补漏机会,可以针对性补强开发者的知识盲区,纠正不良习惯。

    通过 Code Review,我发现了一位同事在 Promise 和函数执行栈方面存在知识盲区,然后我借机帮他补齐了这方面的知识。接下来,我把这个问题分享到技术研讨群,还有几位同学也不是很清楚,也趁机补齐了。于是,我厂再出现这个问题的概率,就降低了。换言之,我厂技术群体的下限,就拔高了。

    思否上有同学问:什么样的技术 leader 是称职的? 我的答案第一条就是:

    给团队的技术兜底。通过工具、规范、流程,保证无论开发水平如何,都能尽快提交符合要求、满足规范、质量过硬的代码。

    具体一点:要重视每一次 Code Review,找到问题,解决问题,补全大家的知识点,提升团队下限。

    5. 总结

    上次的 Code Review 分享获得了意料之外的欢迎,希望这次同样能帮助到大家——我已经把上面的问题加入我的面试题库了,哈哈。

    我现在在 code.fun 工作,我们的产品会把设计稿自动转换成代码,期望大大提升前端的开发效率,如果你对这个产品感兴趣,欢迎联系我,或直接试用。我们公司提供远程工作岗位,有兴趣的同学可以联系我;朋友的公司 API7 也在招聘前端,有兴趣可以找我内推。

    如果诸位读者老爷对软件质量管理、软件开发效率、Code Review 有什么想问的、想说的,敬请在评论区与我互动。

    6. 扩展阅读