Menu

TypeScript try catch:错误类型与自定义错误

TypeScript 中的错误处理:为什么 catch 变量是 unknown,如何用 instanceof Error 收窄它,如何抛出错误,如何编写带 name 和 cause 的自定义错误类,以及处理预期错误的 Result 类型模式。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

TypeScript 使用的就是 JavaScript 的 try、catch、finally 和 throw。TypeScript 特有的部分是 catch 变量:开启 strict 时,它的类型是 unknown,所以使用之前要先检查抛出的是什么。

运行时方面的内容(堆栈如何展开、有哪些内置错误类型)见 JavaScript try/catch。

为什么 catch 变量是 unknown

JavaScript 可以抛出任何东西:一个 Error、一个字符串、一个数字、undefined,或者某个库里的对象。TypeScript 无法知道是哪一种,所以在 strict(即 useUnknownInCatchVariables 标志)下,这个变量是 unknown,使用前必须先收窄:

index.ts(5,34): error TS18046: 'e' is of type 'unknown'.

不开启 strict 时,e 是 any,同样的代码能通过编译,但一旦抛出的是字符串,运行时就会出问题:e.message 是 undefined,再往下访问,比如 e.message.length,就会抛出 TypeError。你也没法靠类型标注绕过去:catch (e: Error) 会报错 TS1196(Catch clause variable type annotation must be 'any' or 'unknown' if specified)。

收窄错误类型

instanceof Error 覆盖所有基于 Error 的类型,包括 TypeError、SyntaxError、RangeError 以及你自己的子类。其他情况就退而把值转换成字符串。写一个小的辅助函数可以让调用处保持简洁:

只处理部分错误的 catch 应该把其余错误重新抛出:if (!(e instanceof ValidationError)) throw e;。吞掉未知错误会掩盖真正的 bug。

抛出错误

throw 接受任何表达式,TypeScript 也不做限制。但还是应该抛出 Error 对象:它们带有堆栈跟踪,代码库中每一个 instanceof Error 检查都依赖于此。

一个总是抛出错误的函数,返回类型是 never,TypeScript 会在调用之后利用它进行类型收窄:

从 ES2022 开始,Error 接受第二个参数,其中可以带上 cause,用来把底层错误串联到你抛出的错误上:throw new Error("could not load settings", { cause: e })。调用方可以读取 err.cause(类型为 unknown)。

自定义错误类

Error 的子类让调用方能用 instanceof 区分不同的失败,并携带额外的数据。记得设置 name,因为继承来的 name 是 "Error",它会出现在日志和 String(err) 中:

先检查最具体的类,因为 NotFoundError 同时也是 HttpError。这里的 override 修饰符是可选的,除非开启了 noImplicitOverride。

旧的教程会在每个错误类的构造函数里加上 Object.setPrototypeOf(this, new.target.prototype)。这在把类编译成 ES5 函数时是必需的,因为那时 Error 子类的 instanceof 会失效。TypeScript 7 已经不再支持 target: "es5"(它会报告 TS5108: Option 'target=ES5' has been removed),而在 ES2015 及以后,原生的 class ... extends Error 可以正常工作,如上所示。

Result 模式

TypeScript 不会跟踪一个函数可能抛出哪些错误,所以没有任何东西会提醒调用方去处理它们。对于预期之中的失败(无效的输入、找不到的记录),返回一个表示“成功或失败”的值,就能把错误纳入类型系统:

在检查 r.ok 之前读取 r.value 会产生编译错误,这正是目的所在。意料之外的情况(bug、断开的连接)用异常处理,调用方必须做出决定的结果则用 Result 值表示。

异步代码中的错误

被拒绝的 promise 会在 await 处变成抛出的错误,所以在 await 外包一层 try/catch 的用法完全一样,变量同样是 unknown。唯一的区别是 promise 上的 .catch():它的回调参数类型是 any 而不是 unknown,所以要自己加上标注(.catch((e: unknown) => ...))。示例见 async/await。

常见错误

  • 不收窄就读取 e.message。 在 strict 下无法编译;不开 strict 时,遇到非 Error 的抛出值就会出错。
  • 捕获所有错误后继续执行。 只处理你预期的错误,其余的重新抛出。
  • 自定义错误忘了设置 name。 这样日志里所有错误都显示为 Error。
  • 抛出字符串。 throw "failed" 没有堆栈跟踪,而且无法通过 instanceof Error 检查。

常见问题

TypeScript catch 块中错误的类型是什么?

开启 strict(即 useUnknownInCatchVariables 标志)时是 unknown,否则是 any。JavaScript 可以抛出任何值,不只是 Error 对象,所以 TypeScript 要求你先检查。在读取 e.message 之前,用 if (e instanceof Error) 收窄它。

TypeScript 中能把 catch 变量的类型标注为 Error 吗?

不能。catch (e: Error) 会报错 TS1196: Catch clause variable type annotation must be 'any' or 'unknown' if specified。只允许 unknown 和 any,因为没有任何东西能保证抛出的是什么。应该在块内部进行收窄。

TypeScript 中如何抛出错误?

和 JavaScript 一样:throw new Error("message"),或者抛出内置或自定义子类的实例,例如 new RangeError(...)。TypeScript 允许你抛出任何值,但抛出 Error 对象能保留堆栈跟踪,也能让 instanceof Error 检查生效。

TypeScript 中如何创建自定义错误类?

继承 Error,调用 super(message, options),并设置 name:class NotFoundError extends Error { name = "NotFoundError"; }。额外的字段放在构造函数里。在任何受支持的编译目标下(ES2015 及以后),instanceof NotFoundError 都能正常工作,不需要修复原型。

TypeScript 有受检异常或 throws 子句吗?

没有。函数的类型不会说明它可能抛出什么,编译器也从不检查错误是否被处理。对于调用方应当处理的错误,用 Result 类型(成功与失败的可辨识联合类型)把它们作为值返回,这样类型系统就会进行检查。

Coddy 编程语言插图

和 Coddy 一起学编程

开始