为了方便讲解,我们先来复习一下错误处理的必须武器 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
此区块是无论如何都会执行到的,try或 catch 区块执行完就会进来,用来处理一些无论有没有错误都要做的事情 (比如把 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
)。
为什么错误处理是最容易被忽略的一块
回归到开头的问题,为什么错误处理往往是最容易被忽略的一块?
相对陌生:\ 网路上的教学影片,实体课程里面的教材,甚至 StackOverflow 等技术讨论论坛,绝大多数的篇幅也都是在教如何「写出你想要的 feature」,而非「修补可能发生的 exception」。
黑天鹅:\ 解决问题的第一步,是发现并重现问题,这样修好了才知道自己不是蒙到。但很多错误是在特定环境才会发生,比如直接操作 A 没事,但先操作 B 再操作 A 就会错误。甚至要是极端情况 (极短时间、大量资料) 才发生,因此光是「产生」这种例外就很困难了。
看不见的防护网:\ 就算真的把这些难缠的问题搞定了,成果也很难被看见,因为对于使用者 / PM 来说,发生错误令人烦躁,但没发生错误却像是基本,也许只有 20% 的时间会发生错误,但我们却需要花费 80% 的时间去处理。
类比来说,其实错误处理很像是现实社会中的「保险」:
相对陌生:\ 许多人常常「主动」去自学股票、网页、厨艺等学问,而保险也是一门学问,但有趣的是,大部分接触到保险的人属于「被动」接触,也就是迫于需要,不得不去理解,往往是生活中出现漏洞才会想到。
黑天鹅:\ 大家都知道保险重要,因为永远不知道明天跟意外哪个先到,尤其意外愈严重愈重要,但严重的意外本来就很少见,当我们意识到时,往往已经深陷泥沼。
看不见的防护网:\ 即便我们真的把保险买齐了,医疗意外储蓄一应俱全,甚至连长照险都超前部署,生活品质短时间仍然不会有变化,毕竟保险确保的是,即便意外发生,仍然确保你有一定的生活品质。
当然啦,错误处理跟保险还是有很多细节、情境的不同,不可完全类比,这边只是笔者有感而发 XD,觉得有些事情,我们在写程式的时候会碰到,在现实生活中也会碰到。
结语
学好Error Handling ,给自己code加一份保险吧~下课啰。
预告....(被打),下期讲「未知与空值 undefined、null、NaN」
原文:https://juejin.cn/post/7094653661313237028