如何在 JavaScript 中创建自定义错误
捕获错误很简单。但要知道捕获了哪个错误——以及原因——才是真正棘手的地方。当你的 catch 块以相同的方式处理数据库超时和无效用户输入时,调试就变成了猜谜游戏。
自定义 JavaScript 错误可以解决这个问题。它们让你能够创建结构化、可识别的错误类型,在应用程序中传递有意义的上下文信息。本指南介绍了使用 ES2022+ 特性的现代方法,包括用于错误链的 Error.cause 以及在当前浏览器和运行时环境中可靠工作的 JavaScript 错误类。
核心要点
- 使用
class ... extends Error语法扩展原生Error类,创建简洁、可维护的自定义错误 - 将
message和options都传递给super(),以保留堆栈跟踪并启用错误链 - 使用
Error.cause(ES2022+)包装并保留来自外部源的原始错误 - 添加结构化字段如
statusCode或field,而不是在消息字符串中编码上下文信息 - 保持错误层次结构扁平——一个基础错误类加上几个专用子类型就能满足大多数需求
标准模式:扩展 Error 类
JavaScript 中的现代错误处理从类语法开始。忘掉传统的原型操作——class ... extends Error 是基准方法:
class ValidationError extends Error {
constructor(message, options) {
super(message, options)
this.name = 'ValidationError'
}
}
super(message, options) 调用至关重要。它将消息和选项对象都传递给父类 Error 构造函数,后者会自动处理堆栈跟踪的捕获。
设置 this.name 可确保你的错误在堆栈跟踪和日志中正确标识自己。如果没有它,你的自定义错误会显示为通用的 “Error”。
添加结构化字段
纯消息字符串很脆弱。解析 “Invalid email: user@” 来提取上下文容易出错。相反,应该添加结构化字段:
class HttpError extends Error {
constructor(message, statusCode, options) {
super(message, options)
this.name = 'HttpError'
this.statusCode = statusCode
}
}
throw new HttpError('Resource not found', 404)
现在你的错误处理程序可以直接检查 error.statusCode,而不是解析字符串。这种模式可以很好地扩展——添加 details、code 或应用程序需要的任何特定领域字段。
使用 Error.cause 进行错误链
当包装来自外部源(数据库、API、库)的错误时,你需要保留原始错误。ES2022 引入的 cause 选项可以处理这个问题:
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) {
throw new HttpError('User fetch failed', response.status)
}
return response.json()
} catch (error) {
throw new HttpError('Unable to load user', 500, { cause: error })
}
}
原始错误通过 error.cause 保持可访问,为调试保留了完整的错误链。这在现代浏览器和 Node.js 中都受支持——无需 polyfill。
Discover how at OpenReplay.com.
构建小型错误层次结构
JavaScript 错误层次结构在保持扁平时效果最好。一个基础应用程序错误类加上几个专用子类型就能满足大多数需求:
class AppError extends Error {
constructor(message, options = {}) {
super(message, { cause: options.cause })
this.name = this.constructor.name
this.statusCode = options.statusCode ?? 500
}
}
class NotFoundError extends AppError {
constructor(message, options = {}) {
super(message, { ...options, statusCode: 404 })
}
}
class ValidationError extends AppError {
constructor(message, field, options = {}) {
super(message, { ...options, statusCode: 400 })
this.field = field
}
}
使用 this.constructor.name 会自动从类名设置错误名称,减少子类中的样板代码。
在异步流程中区分错误
自定义错误在需要处理不同失败模式的异步代码中表现出色:
try {
const user = await fetchUser(userId)
} catch (error) {
if (error instanceof NotFoundError) {
return { status: 404, body: { message: 'User not found' } }
}
if (error instanceof ValidationError) {
return { status: 400, body: { message: error.message, field: error.field } }
}
// 意外错误——记录日志并返回通用响应
console.error('Unexpected error:', error.cause ?? error)
return { status: 500, body: { message: 'Internal error' } }
}
instanceof 检查在继承链中正确工作。在处理序列化错误时,你也可以通过检查自定义字段或 error.name 来区分错误。
对于涉及多个同时失败的场景,AggregateError 提供了一种标准方式来捆绑错误——对于多个操作可以独立失败的并行操作很有用。
TypeScript 注意事项
如果你使用 TypeScript,请在 tsconfig.json 中设置 target: "ES2022" 或更高版本,或在 lib 数组中包含 "ES2022"。这可以确保 ErrorOptions 接口中的 cause 属性具有正确的类型。
结论
使用 class ... extends Error 构建的自定义 JavaScript 错误为你提供了结构化、可识别的异常,可以在应用程序中传递上下文信息。将 message 和 options 传递给 super(),使用 cause 进行错误链,并添加特定领域的字段而不是在消息字符串中编码信息。保持层次结构扁平,你的错误处理就会变得可预测且易于调试。
常见问题
自定义错误让你能够以编程方式区分不同的失败类型。你可以使用 instanceof 检查或检查自定义属性(如 statusCode 或 field),而不是解析错误消息来确定出了什么问题。这使得错误处理更可靠,代码更易于调试和维护。
Error.cause 在 ES2022 中引入,在所有现代浏览器和 Node.js 16.9+ 中都受支持。对于较旧的环境,cause 选项会被简单忽略——你的代码仍然可以工作,但 cause 属性将是 undefined。只有在必须支持旧版运行时的情况下才考虑使用 polyfill。
保持扁平——通常最多两层。一个基础应用程序错误类加上少数几个专用子类型就能满足大多数用例。深层次结构会增加复杂性而没有相应的好处,并且随着应用程序的发展会使重构变得更困难。
可以。当多个操作可以独立失败时,将单个失败包装在自定义错误中,然后使用 AggregateError 将它们捆绑在一起。errors 数组中的每个错误都保留其类型,因此在处理结果时仍然可以使用 instanceof 检查。
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.