Day 17 - Error Handling 错误处理

前言

错误处理往往是最容易被忽略的一块,因为

  • 程序运行顺利,那当然不用考虑 error case
  • 程序被测出 bug,通常是程序码里面有问题,QA 会催着你赶快去修,修完之後就回到第一点,那个「运行顺利」的版本了,好像又不用错误处理了

这样到底什麽时候要错误处理?

为了方便讲解,我们先来复习一下错误处理的必须武器 try catch,如果你已经很熟了可以跳过这一段:

try/catch/finally

try/catch/finally 属於流程控制的逻辑区块(跟 if/else 一样),以下分别介绍:

openFile();
try {
  writeFile(theData); // 可能产生例外
} catch(e) {
  handleError(e); // 处理可能发生的例外
} finally {
  closeFile(); // 总是在 try 结束後关闭档案
}
  • try
    此区块是主要的程序执行区块,只要在这个区块的任何一行抛出错误(无论有没有用到 throw),就不会继续往下执行,而是跳到 catch 区块第一行开始执行。
  • catch
    此区块可以在错误发生时,自动捕捉进来这个区块,会进来代表「出代志」了,通常会针对错误做一些对症下药或善後处理,起码可以确保程序不会直接死当。
  • finally
    此区块是无论如何都会执行到的,trycatch 区块执行完就会进来,用来处理一些无论有没有错误都要做的事情(比如把 loading 关掉、写 log 等)。

错误的种类

来谈谈错误的种类,了解一下「错误」到底是什麽?才会知道该怎麽处理它:

程序开发者的错误(programmer errors):

即程序本身的 bug,错误是程序本身没写对造成的。常见的例如:

  • 语法错误(syntax error):少括号、关键字拼错等
  • 取值错误(reference error):变数、函式忘记宣告就使用
  • 类型错误(type error):在 Number 类型的变数使用 Object 的函式

这类型的错误代表与开发者的意图背道而驰,因此没有悬念,就是一定要把它改对,通常透过开发人员工具可以轻松找到原因。

运算的错误(operational errors)

在程序本身没有 bug 的情况下,错误发生在系统本身,通常是程序与外部互动下发生,外部可能是使用者、网路远端、档案系统。常见的例如:

  • 使用者输入极端值
  • 网路连线问题
  • 记忆体超出负荷

这类型的错误代表,通常在使用者「预想」情况下,程序是可以正常运作的,但是如果:

十笔资料可以跑,那十万笔呢?
都市可以跑,那偏乡地区呢?
json 档案可以处理,那 xml 呢?

因此,其实这类型的错误不太算是「错误」,毕竟部分的 case 都还是可以运作的,或许比较适合称呼为例外(exception)。

这类型的错误处理起来就复杂许多,需要对症下药,

  • initial:针对首次使用服务可能会没有初始值的问题,可以视情况先做 initial 动作。
let file;
try {
    file = readFile(filePath);
} catch (err){
    file = createFile(filePath);
}
console.log(file);
  • retry:针对网路连线类型的问题,可以限制次数 retry。
let retryCount = 0;
const retryMax = 3;
do {
    try {
        console.log(retryCount);
        // ...
        // 可能失败的 code 放这 
        // ...
        break;
    } catch (err){
        console.log(err);
        retryCount++;
        if (retryCount >= retryMax) {
            // 超过 retry 次数强制跳出
            break;
        } else {
            // 还没超过可以再试一次
            continue;
        }
    }
} while (true);
  • unknown:面对未知的问题,都应该要有最後一道防线,发生例外时记下 Log,跳出 toast 讯息提醒前端,并关掉 loading 让使用者仍然可以进行其他操作等。
setLoading(true);
try {
    // ...
    // 可能失败的 code 放这 
    // ...
} catch (err){
    logToDB(err);
} finally {
    setLoading(false);
    toast('系统不稳,请稍後再试');
}

我可以用 if/else 来处理错误吗?

未知的问题在 catch 处理很合理,有一些已知的问题也放在 catch 处理,但既然我都知道这边有可能会出问题了,为何不乾脆用 if/else 判断处理?

这个问题其实我也觉得稍微模糊,我的想法是,需要先去定义出,catch 究竟要接收什麽?是所有取不回资料的状况吗?还是针对非正常流程的处理?针对不同的目的,应该使用不同的处理,以下是我的一些想法

适合放在 catch 处理的:

  • I/O 时发生错误,比如: 读写档案、fetch 资料途中,发生例外情况
  • 无法完成预期的工作,比如: 想要读取登入後的画面,但因为还没登入导致的错误
  • 内部错误,比如後端自己死掉,或是前端传送错误参数

不适合放在 catch 处理的(可以用 if/else 处理):

  • 查不到资料(而非查资料失败)的情况,比如: 资料库查询,query 不到东西的情况下,需要处理空值,而非抛出错误。
  • 询问类,比如: 手机要读取联络人清单的权限,当回传的结果是 false,也就是使用者不授予权限时。

但这也不是硬性规定,单纯是不同的想法导致不同的设计,甚至不同的 API 在处理错误的状态也都不尽相同,因此笔者认为,只要整个 app 在面对错误是一致就可以了 (比如查无资料要嘛都放 else,要嘛都放 catch)。

为什麽错误处理是最容易被忽略的一块

回归到开头的问题,为什麽错误处理往往是最容易被忽略的一块?

  1. 相对陌生
    网路上的教学影片,实体课程里面的教材,甚至 StackOverflow 等技术讨论论坛,绝大多数的篇幅也都是在教如何「写出你想要的 feature」,而非「修补可能发生的 exception」。
  2. 黑天鹅
    解决问题的第一步,是发现并重现问题,这样修好了才知道自己不是蒙到。但很多错误是在特定环境才会发生,比如直接操作 A 没事,但先操作 B 再操作 A 就会错误。甚至要是极端情况(极短时间、大量资料)才发生,因此光是「产生」这种例外就很困难了。
  3. 看不见的防护网
    就算真的把这些难缠的问题搞定了,成果也很难被看见,因为对於使用者/PM 来说,发生错误令人烦躁,但没发生错误却像是基本,也许只有 20% 的时间会发生错误,但我们却需要花费 80% 的时间去处理。

类比来说,其实错误处理很像是现实社会中的「保险」:

  1. 相对陌生
    许多人常常「主动」去自学股票、网页、厨艺等学问,而保险也是一门学问,但有趣的是,大部分接触到保险的人属於「被动」接触,也就是迫於需要,不得不去理解,往往是生活中出现漏洞才会想到。
  2. 黑天鹅
    大家都知道保险重要,因为永远不知道明天跟意外哪个先到,尤其意外愈严重愈重要,但严重的意外本来就很少见,当我们意识到时,往往已经深陷泥沼。
  3. 看不见的防护网
    即便我们真的把保险买齐了,医疗意外储蓄一应俱全,甚至连长照险都超前部署,生活品质短时间仍然不会有变化,毕竟保险确保的是,即便意外发生,仍然确保你有一定的生活品质。

当然啦,错误处理跟保险还是有很多细节、情境的不同,不可完全类比,这边只是笔者有感而发XD,觉得有些事情,我们在写程序的时候会碰到,在现实生活中也会碰到。

结语

错误/例外处理是很多开发者不太想去碰的一块,很容易只关注在「正常」,而比较少考虑「异常」。

市场上大部分的商业专案、产品,PM 当然也都是要求先有功能,毕竟没功能的话,就算有一堆 try catch 也是枉然。

但正是因为「少而重要」,才能体现出价值,如果能够在这点更谨慎,无疑是朝向「更好」的 developer 迈进!

愈深的洞穴里
藏着愈闪耀的宝石

参考资料

MDN - 例外处理


<<:  整合架构说明

>>:  用React刻自己的投资Dashboard Day17 - Dashboard 2.0版路由功能

[Day 08 - CSS Architecture] CSS设计模式,摆脱义大利面代码

当别人说你的程序是义大利面代码 (Spaghetti code),听起来很好吃,应该是称赞的意思吧!...

Day 12 Classify images with the Custom Vision service

Some potential uses - Product identification, disa...

食谱搜寻系统制作_上

制作目标 搜寻选择、使用者输入 Node.js从版本7开始提供readline模组,从可读串(例如p...

pure component

在写 Vue.js 或 React 的过程,一定都会听过这个名词。 并不是所有的 component...

[Day - 19] - Spring 例外处理之优雅化客制化错误讯息原理与设计

Abstract 相信许多人都有遇过例外处理的事件,但又不想要让内部核心的错误资讯呈现在前端上,仅记...