使用Google Voice服务可以免费拨打美国或加拿大境内的任何电话,而国际电话(非北美地区)的费用也比传统电话便宜,比如拨打中国大陆的价格为1美分/分钟,香港的价格为2美分/分钟,台湾市话:2美...
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
此区块是无论如何都会执行到的,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,觉得有些事情,我们在写程序的时候会碰到,在现实生活中也会碰到。
结语
错误/例外处理是很多开发者不太想去碰的一块,很容易只关注在「正常」,而比较少考虑「异常」。
市场上大部分的商业专案、产品,PM 当然也都是要求先有功能,毕竟没功能的话,就算有一堆 try catch 也是枉然。
但正是因为「少而重要」,才能体现出价值,如果能够在这点更谨慎,无疑是朝向「更好」的 developer 迈进!
愈深的洞穴里
藏着愈闪耀的宝石
参考资料
国内公司、香港公司、美国公司是目前绝大部分跨境卖家的身份选择。国内公司身份自然不必多说,90%+都是。香港和美国公司则少的多。但选择的人多并不表示国内公司就是最佳的经营跨境电商身份选择。 国内公司 首...
在深入探讨提高网站速度之前,让我们探讨一下为什么它对您的小型企业如此重要。 更快的网站意味着: 更好的用户体验 您的网站性能会影响用户的体验 - 当您的网站加载速度更快时,用户更有可能与之互动并花费更...
网站速度优化对于创造积极的用户体验至关重要。 积极的用户体验是快乐用户的营销代言词。 快乐的用户访问您的网站并购买东西。 不满意的用户离开是因为他们厌倦了等待您的网站加载。 营销人员称之为“跳出率”...
为什么网站速度很重要 到目前为止,您应该不需要说服网站速度对您的在线业务至关重要。 这是因为网站性能会影响您的品牌声誉、SEO 排名和转化率。 以下是发生这种情况的主要原因: 品牌口碑👍 老实说,当...