聊聊错误/异常处理

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

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

总结

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

如果您觉得文章内容对您有用,不妨支持我创作更多有价值的分享:


已发布

分类

来自

评论

欢迎吐槽,共同进步

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据