k8w.io
try...catch 对JS的性能影响有多大?
2020-11-17作者:k8w

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

通常错误可以分为业务错误(例如余额不足)和非业务错误(例如网络错误)。
业务错误通常使用方式二返回,而非业务错误通常通过方式一返回。
但对于前台用户而言,其实并不关注具体错误细节,通常是有着统一的处理方式(例如弹窗提示“系统繁忙,请稍后再试”)。
所以对于前台系统,往往业务错误和非业务错误采用相同的处理方式,这部分代码显然可以复用。因而一种高效的方式是,将两种错误统一封装为方式一或方式二来抛出,可以简化开发流程。

(正文完)
留言(0条)
发表新留言
您的大名:
必填
电子邮箱:
不公开,仅用于向你发送回复