try...catch
是很多编程语言中常见的一种写法,JS也不例外。
什么时候应该使用 try...catch
,它对性能的影响又有多大?
错误的分类
在开发过程中,我们一定会遇到很多“错误”,可以大致分为以下这么几种:
- 可以预期的业务错误(例如余额不足,没有权限,登录态过期等)
- 代码错误(例如使用了未声明的变量,没有进行空值检测等)
- 网络错误(例如加载失败、超时、跨域警告等)
- 其它预料之外的错误
通常来说,处理这些错误有两种方式。
处理方式一:try…catch…
try
{
// 在此运行代码,抛出异常
throw new Error('错误信息')
}
catch(err)
{
// 在此处理异常
console.log(err);
}
使用 try...catch
语句包裹住预期可能出现异常的方法,然后在 catch
中处理。
处理方式二:在返回值中包含错误
例如很多后台接口,喜欢将返回值定义为如下类型:
interface Response {
// 返回值为0说明成功,data有值
// 否则说明出错,errmsg为错误信息,data无值
errcode: number,
errmsg?: string,
data?: any
}
如此,调用方在获得返回值后,根据错误标识字段,即可判断是否出错。
两种处理方式的区别
错误检测的强制性
try...catch
本质上是给了开发者选择:你可以处理这个异常,也可以选择不处理。如果不处理,则异常会抛至全局作为“未经捕获的异常”。
例如对于以下方法,如果使用TypeScript:
function test(): string {
throw new Error('XXXXX')
}
我们虽然明知道 test()
可能会抛出异常,但在特定环境下,我们可以选择不处理,例如:
- 由于经验和推断确认此处不可能出错,于是不额外耗费代码量处理
- 可能是管理类私有程序,更强调开发效率,并且无需处理错误(大不了重试呗)
- 不应该出现此类错误,如果真出现,反而应该在开发阶段更明显的暴露出来(因为try…catch处理不得当很可能就把错误掩盖了,感知不到了)
- 此处并不能确定错误的处理方式,交由更外层去处理(异常会层层上抛)
// result 的类型依旧被自动推断为 string
let result = test();
// TS编译器不会报错
console.log(result.substr(5));
但如果选择了方式二,以下代码则不同。
function test(): { isSucc: true, data: string } | { isSucc: false, errMsg: string } {
return { isSucc: false, errMsg: '未知错误' }
}
let result = test();
// TS编译器会报错
// 因为当result.isSucc为false时,result.data可能不存在
console.log(result.data.substr(5));
必须利用TypeScript强大的控制流分析,给与类型检测保护:
function test(): { isSucc: true, data: string } | { isSucc: false, errMsg: string } {
return { isSucc: false, errMsg: '未知错误' }
}
let result = test();
if (result.isSucc) { // 检测确保没有报错
console.log(result.data.substr(5));
}
else{
// 在此处处理错误的情况
console.log(result.errMsg)
}
可见,两种方式的主要区别在于:
try...catch
给用户选择,用户可以选择忽略不处理错误。- 而方式二将错误信息带在返回值中,则可以利用TS的类型检查特性强制开发者必须处理异常。
- 二者各有利弊,
try...catch
更为灵活,而方式二更为严谨(例如将方式二作为项目规范,则可以有效避免没有经验的开发人员,忽略处理错误的情况)
性能对比
在V8引擎中,try..catch
内的代码不会被JIT优化,所以try...catch
会有一定额外的性能损失,具体损失有多大呢,见如下代码对比。
用try…catch
function test(i) {
if (i % 2) {
return { isSucc: true }
}
else {
throw new Error('错误原因');
}
}
console.time('trycatch');
let succ = 0, fail = 0;
for (let i = 0; i < 100000; ++i) {
try {
let result = test(i);
++succ;
}
catch (e) {
++fail;
}
}
console.timeEnd('trycatch');
不用try…catch
function test(i) {
if (i % 2) {
return { isSucc: true }
}
else {
return { isSucc: false, errMsg: '错误原因' }
}
}
console.time('no-trycatch');
let succ = 0, fail = 0;
for (let i = 0; i < 100000; ++i){
let result = test(i);
if (result.isSucc) {
++succ;
}
else {
++fail;
}
}
console.timeEnd('no-trycatch');
结论
同上两个功能完全相同的函数执行10万次,在NodeJS上运行,结果:
- 使用try…catch,平均时间700ms左右
- 不适用try…catch,平均时间5ms左右
所以,如果你的场景是低频调用场景,那么try…catch的代价可以忽略不计。
如果是高频场景,例如你想做一个单机10W QPS的后台服务,那么try…catch就会有非常昂贵的代价了。
不过凡事无绝对,即便是高频场景,如果抛出异常的概率很低,那么try…catch的影响也十分有限。
经测试,只有catch内的部分,无法进行JIT优化,例如以下代码:
function test(i) {
if (i % 1000) {
return { isSucc: true }
}
else {
throw new Error('错误原因');
}
}
console.time('trycatch');
let succ = 0, fail = 0;
for (let i = 0; i < 100000; ++i) {
try {
let result = test(i);
++succ;
}
catch (e) {
++fail;
}
}
console.timeEnd('trycatch');
平均运行时间 5ms
虽然也使用了 try...catch
,但由于异常的抛出概率只有千分之一,所以对性能几乎没有影响。
错误和异常处理的小Tips
通常错误可以分为业务错误(例如余额不足)和非业务错误(例如网络错误)。
业务错误通常使用方式二返回,而非业务错误通常通过方式一返回。
但对于前台用户而言,其实并不关注具体错误细节,通常是有着统一的处理方式(例如弹窗提示“系统繁忙,请稍后再试”)。
所以对于前台系统,往往业务错误和非业务错误采用相同的处理方式,这部分代码显然可以复用。因而一种高效的方式是,将两种错误统一封装为方式一或方式二来抛出,可以简化开发流程。