首页 / 文章 / 为什么在 JavaScript 中 `catch()` 会抛出 SyntaxError 错误

为什么在 JavaScript 中 `catch()` 会抛出 SyntaxError 错误

了解为何空的 catch 参数列表会完全破坏 JavaScript 的解析机制,并查看编写无参数 catch 块的两种符合语法的正确方式。

1836 词

乍看之下,这段代码似乎会输出“Error”字样。实际上整个文件都会因SyntaxError而报错,因为在使用括号时内部不写任何内容的catch ()格式从来都不是有效的JavaScript代码。

想象一下第二轮前端面试的场景。原本看似简单的热身题却变成了更棘手的问题。面试官要求实现一个能抛出异常并在catch块中记录信息的try-catch结构,同时还给出了提示:“你不需要错误对象。”

如果不需要错误对象,最自然的做法就是直接跳过它的声明。多年以来习惯于编写如function handler() {}这样的代码,会让人自然而然地选择不填写参数列表:

try {
  throw "Error";
} catch () {
  console.log('Error')
}

当被问及这段代码会输出什么时,显而易见的答案似乎是“Error”——异常被抛出,捕获块执行,随后console.log会被调用。但当面试官真正运行这段代码时:

Uncaught SyntaxError: Unexpected token ')'

没有任何内容被打印出来,既没有“Error”,也没有其他任何信息。脚本甚至没有执行过任何一条语句。就在这时,面试官说出了本文标题的来源:

你拥有五年JavaScript开发经验,却不知道try-catch块是如何工作的。

那并非一个问题,而是一句陈述——而且是很令人不适的陈述,因为它恰好是事实。在五年编写JavaScript的经验中,这位开发者从未输入过catch ()。即使该参数实际上从未被引用,它也总是有名称的。第一次尝试省略它时,就暴露出了一个他根本从未学过的规则。

catch 只有两种合法形式

catch 子句的正式语法(ECMA-262第14.15节“try语句”)相当简洁:

Catch :
  catch ( CatchParameter ) Block
  catch Block
CatchParameter :
  BindingIdentifier
  BindingPattern

仔细查看第一个生成规则。当存在括号时,必须在括号内使用 CatchParameter。语法中并未将其标记为可选项。它必须对应唯一的绑定形式:要么是像 e 这样的普通标识符,要么是像 {message} 这样的解构模式。空括号则不符合任何一种生成规则。

第二个生成规则是在 ES2019 中引入的,它完全去掉了括号。空括号同样不符合任何规则。因此,当解析器读取到 catch ( 时,它期望紧接着出现有效的绑定标记,但却遇到了 ),于是停止解析。这正是 V8 抛出的错误信息:Unexpected token ')'

这引出了一个合理的问题:为什么 function f() {} 能正常编译,而 catch () {} 却始终无法编译?函数的参数列表遵循 FormalParameters 语法,该语法允许零个参数、默认值以及剩余参数语法。而 CatchParameter 是被刻意设计成不同的形式——它仅表示一个必需的绑定,没有用逗号分隔的参数列表,也没有默认值或剩余参数元素。Catch 不存在空参数列表的概念,因此那种适用于函数的“只需把括号里的内容清空”这一思维捷径在这里并不适用。

此错误会在代码运行之前出现

这个错误中还隐藏着另一个误解:将异常视为纯粹的运行时现象。实际上这种故障发生在解析阶段,甚至在程序开始执行之前就已经出现。引擎会扫描整个脚本,无法使其符合语法规范,因此在任何一行代码有机会被执行之前就会报告SyntaxError错误。文件中的其他所有内容也会因此受到影响。

再加一行代码就能让这个问题更加明显。在Chrome中已验证了以下情况:

console.log('before');            // NEVER runs
try { throw "Error"; } catch() { }
Uncaught SyntaxError: Unexpected token ')'

console.log('before')这行代码位于格式错误的catch子句之上,它本身并无问题,但却依然不会被输出。由于整个脚本都无法编译,所以没有任何代码能够运行。

这会导致一个乍看之下容易被忽略的结论:文件内的 try-catch 块无法捕获来自同一文件的 SyntaxError。任何 catch 块要能够执行,其周围的代码就必须已经成功解析,而这正是出错的地方。要捕获这类错误,唯一的办法是通过 evalnew Function 或动态的 import(),从完全独立的编译单元中进行捕获。

两种正确的编写方式

下面的两种方法都在 Chrome 中得到验证,且都是有效的。

选项 1:保留参数,但不使用它。 声明一个从不被引用的 catch 参数是完全合法的,在所有版本的 ECMAScript 中一直都是如此。

try {
  throw "Error";
} catch (e) {
  console.log('caught, e unused');
}
// logs: caught, e unused

选项2:完全去掉括号。这就是ES2019的可选catch绑定语法:无需括号,也无需参数,只需写成catch {即可。

try {
  throw "Error";
} catch {
  console.log('caught without binding');
}
// logs: caught without binding

需要牢记的思维模式是:简化catch (e)意味着去掉括号,而非清除括号内的内容。这两种有效形式之间的中间状态正是会导致SyntaxError的原因所在。

无括号catch语法的起源

这种可选catch绑定最初是由Michael Ficarra提出的TC39提案。它在2018年1月进入了第4阶段,最终成为ES2019的一部分。浏览器和运行时环境也很快提供了支持:Chrome 66、Firefox 58、Safari 11.1以及Node.js 10都支持该语法,这意味着在当今几乎所有的部署环境中都可以安全地使用它。

该提议背后的思路与让面试候选人犯难的场景完全一致:即那些实际上根本不需要捕获错误的代码。比如检查字符串是否能解析为有效的 JSON,若不能则使用默认值;又或者在一些特征检测场景中,只需捕获异常就能获得所有所需信息。在这类情况下,将错误命名为 e 只是创建了一个会被赋值但从未被读取的变量——而该提议本身也指出,这种模式通常意味着代码的其他地方存在缺陷。

有必要明确该提案究竟做了哪些改动:它为没有参数列表的捕获块引入了新的语法结构。它并未允许使用空括号——这些括号并非被清空,而是完全移除。这样既保持了“当存在 CatchParameter 时必须恰好有一个绑定”的规则,也使得 catch { 在视觉上与从一开始就不带括号的 finally { 保持一致。

为何连经验丰富的开发者也会犯此错误

导致人们犯错的原因有三个,且都与缺乏努力或学习无关。

你的直觉源自另一种更为熟悉的模式——而这种直觉会误导你。你写过的每一个函数签名都在强化这样一个观念:未使用的参数列表可以简化为空括号:function () {}是完全正常的。由于catch语句在视觉上与函数头部相似,使用同样的简写方式显得很自然。但实际上catch语句并不接受参数列表,它只接受一个绑定值,而且其语法中从来就没有过空版本的表述。

这种错误通常要等到你试图移除未使用的 e 时才会显现。这一时刻往往是由代码检查工具的警告触发的。ESLint 的 no-unused-vars 规则包含一个 caughtErrors 选项,从 ESLint 9 开始该选项的默认值为 "all"——这意味着未使用的 catch (e) 会立即产生代码检查错误。为了解决这个警告,开发者们会本能地像处理函数签名那样清空括号,但恰恰是这一点让解析器阻止了他们。真正正确的解决方法——完全删除括号并写成 catch {——却几乎没人会首先采用,因为在 JavaScript 中其他地方,“缩写”并不意味着清空而是删除。

这类错误根本无法存活足够长的时间来留下痕迹。某个细微的逻辑错误可能会混入生产环境,长期困扰代码库数月之久,成为开发者们多年后仍会提及的“经典故事”。但这个错误完全不同:它是在解析阶段就被立即捕获的SyntaxError。你看到下划线提示后,几秒钟内就能修复它,然后继续处理其他事务,根本不会多想。它不会留下任何长期记忆——而这恰恰使其成为一道出色的面试问题,能够检验你是真正掌握了语法,还是只是自以为理解而已。

在您着手修改所有能找到的 catch (e) 语句之前,有一点需要注意:catch { } 允许您跳过为错误命名的步骤,但并不允许您跳过对错误的处理。ES2019 的这种形式是为那些需要通过捕获行为本身来传递有用信号的合理检测与回退逻辑而设计的。无论是否使用括号,那种只是默默吞下真正错误却没有任何处理的空 catch 体依然存在同样严重的问题。

本应更恰当的答案

能让这次访谈继续顺利进行的回答是:“在解析时会抛出 SyntaxError 错误——具体为 Unexpected token ')'。文件中的任何代码都无法执行,就连 try 块之前的代码也不例外。catch 子句只有两种合法形式:catch (binding) { },或者从 ES2019 开始出现的无参数形式 catch { }。空括号不符合这两种格式。”

值得记住的关键点:

  • 带有空括号的 catch () 在所有版本的 JavaScript 中一直都是、且依然是 SyntaxError 错误。
  • 仅有两种有效形式:一种是带有单个命名绑定的 catch (e) { },另一种是从 ES2019 引入的、完全不带括号的 catch { }
  • CatchParameter 是一个必需的单一绑定,而非参数列表——函数语法中“空括号”的用法在此并不适用。
  • 由于这是解析时出现的错误,整个文件都无法运行——同一文件中的其他 try 块也无法拦截该错误。
  • 虽然从技术上讲保留未使用的 ecatch (e) 是可行的,但 ESLint 9 的 no-unused-vars 规则会默认标记其为错误;正确的修复方式是去掉括号,而非清除括号内的内容。
  • 无参数的 catch { } 是为那些确实不需要错误值的情况设计的——它并非用来悄悄忽略真实错误的万能借口。
  • 面试官的严厉反应公平吗?只能说部分公平。try-catch 的运行时行为从来都不是问题——那已经是理所当然的。真正从未被审视的是语法本身,因为日常开发中的代码很少需要人们去处理它。如今情况却变了:现在要删除未使用的 e 时,意味着要同时去掉括号,而不仅仅是清空内容。

    在简化 catch 子句时,要删除括号——绝不能只是清空内容。

    相关阅读

  • React组件入门:构建可复用、易维护的UI组件 — 了解为何将UI拆分为小型React组件能提升复用性、可读性以及团队协作效率,随后动手创建你的第一个功能组件。