先讲个我自己的经历。有一次做代码评审,同事提交了一段很正常的逻辑,里面有几个try...catch兜底,另一个前辈当场说“这玩意伤性能,能不用就别用”。我当时没吭声,但心里一直存疑:try...catch到底伤在哪里?是一个个try块本身在消耗CPU,还是只有真的抛出异常时才慢?后来我在Node 20、Chrome和Deno上分别跑了对比测试,又翻了V8、JavaScriptCore的编译实现细节,才彻底搞明白这件事。结论先放这里:只要不真正进入异常抛出路径,try...catch在现代引擎里的开销基本可以忽略;真正让性能崩掉的,是throw、Error对象创建、错误栈信息收集这一整套异常处理链路。 网上很多“try...catch性能杀手”的言论,多半是把这两件事混为一谈了。
这篇文章就围绕这个被广泛误读的话题展开,我会给出可复现的测试思路、异常机制的成本拆解、热路径上滥用异常的真实场景,以及我后来沉淀下来的一套异常使用规范。无论你是被代码规范“禁了”try...catch的老实人,还是想搞明白异常为什么慢的进阶选手,这篇都能给你一个相对完整的答案。
1. 这个说法不全是谣言:早期V8确实给过性能“下马威”
如果你问一个干了十年以上的老前端,为什么大家这么忌惮try...catch,大概率会听到这样的回答:用了try...catch的函数,V8就不优化了,性能会掉一大截。
这个说法在今天听起来像是玄学,但在2017年之前,它基本是事实。
1.1 旧版本V8的“去优化”机制
V8从诞生到2017年前后,经历了Crankshaft到TurboFan的编译管线迁移。早期Crankshaft作为优化编译器,对代码的控制流图有比较强的限制,包含try...catch这类带异常处理边界的函数,很多优化手段做不了,于是V8直接选择“不优化”这个函数。一个函数如果被高频率调用,同时又带try...catch,它就只能跑在解释器或基础编译器层面,性能比优化后的版本差很多。
这个问题在TurboFan逐渐完善之后才开始缓解。到Node 8.3内置的V8 6.0时代,TurboFan已经能够优化带try...catch的函数。再往后,到了Node 12+(V8 7.4+)、Node 16+(V8 9.x+),异常处理代码的优化水平已经比较成熟。结论是:“try...catch会导致函数不被JIT优化”这个说法,属于旧版本引擎的历史遗留问题,今天的主流运行环境早就不是这样了。
但这不妨碍这个说法在技术社区里像都市传说一样流传。很多团队代码规范里写“禁止使用try...catch”,源头就是当年的这个真实限制,一传十年,谁也没去验证新引擎是否还这样。
1.2 还有一个更容易混淆的点:把throw的开销算到try...catch头上
市面上最常见的“try...catch性能测试”之类文章,普遍是下面这种写法:
javascript复制function test() {
try {
// 每次调用都抛一次异常
throw new Error('boom');
} catch (e) {}
}
然后跑个循环,测出来try...catch比普通代码慢几十倍,得出结论:try...catch性能极差,不要用。
这个测试本身没错,但它测的是异常抛出机制的成本,而不是异常捕获边界的成本。如果你把同一段逻辑改成直接用return -1、return null,当然快得飞起。但这不是“try...catch的锅”,这是“异常机制本来就不是用来做常规流程控制”的。把两者的成本混为一谈,是大量误导性结论最核心的来源。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 实测:不抛异常时,try...catch的开销接近可以忽略
为了让结论可复现,我给了自己一个相对严谨的测试方案:比较同样一段计算逻辑,分别放在有无try...catch包裹下的运行耗时。重点在于测试里不能有任何throw,所有try块都只是“正常走过去”。
2.1 测试设计与代码
我当时用Node 20做了一组测试,核心代码如下:
javascript复制const iterations = 100000000;
function withoutTry() {
let sum = 0;
for (let i = 0; i < iterations;
