遗憾的是,在 Javascript 中实现这一基本功能没有“官方”方式。我将分享我在不同包中看到的三种最常见的解决方案,以及如何在现代 Javascript (es6+) 中实现它们,以及它们的一些优缺点。
1。子类化错误类
在 es6 中子类化“Error”的实例变得更加容易。只需执行以下操作:
class FileNotFoundException extends Error {
constructor(message) {
super(message)
// Not required, but makes uncaught error messages nicer.
this.name = 'FileNotFoundException'
}
}
完整示例:
class FileNotFoundException extends Error {
constructor(message) {
super(message)
// Not required, but makes uncaught error messages nicer.
this.name = 'FileNotFoundException'
}
}
// Example usage
function readFile(path) {
throw new FileNotFoundException(`The file ${path} was not found`)
}
try {
readFile('./example.txt')
} catch (err) {
if (err instanceof FileNotFoundException) {
// Handle the custom exception
console.log(`Could not find the file. Reason: ${err.message}`)
} else {
// Rethrow it - we don't know how to handle it
// The stacktrace won't be changed, because
// that information is attached to the error
// object when it's first constructed.
throw err
}
}
如果您不喜欢将this.name 设置为硬编码字符串,则可以将其设置为this.constructor.name,这将给出您的班级名称。这样做的好处是您的自定义异常的任何子类都不需要同时更新this.name,因为this.constructor.name 将是子类的名称。
与某些替代解决方案相比,子类异常的优势在于它们可以提供更好的编辑器支持(例如自动完成)。您可以轻松地将自定义行为添加到特定的异常类型,例如附加函数、替代构造函数参数等。在提供自定义行为或数据时支持 typescript 也往往更容易。
有很多关于如何正确继承Error 的讨论。例如,如果您使用的是转译器,上述解决方案可能不起作用。如果可用,一些人建议使用特定于平台的 captureStackTrace()(虽然我在使用它时没有注意到错误有任何区别 - 也许它不再那么相关了?♂️)。要了解更多信息,请参阅 this MDN 页面和 This Stackoverflow 答案。
许多浏览器 API 走这条路并抛出自定义异常(可以看到 here)
2。为错误添加区分属性
这个想法很简单。创建你的错误,为你的错误添加一个额外的属性,例如“code”,然后抛出它。
const error = new Error(`The file ${path} was not found`)
error.code = 'NotFound'
throw error
完整示例:
function readFile(path) {
const error = new Error(`The file ${path} was not found`)
error.code = 'NotFound'
throw error
}
try {
readFile('./example.txt')
} catch (err) {
if (err.code === 'NotFound') {
console.log(`Could not find the file. Reason: ${err.message}`)
} else {
throw err
}
}
当然,您可以创建一个辅助函数来删除一些样板并确保一致性。
此解决方案的优点是您无需导出程序包可能抛出的所有可能异常的列表。例如,如果您的包一直使用 NotFound 异常来指示特定函数无法找到预期的资源,您可以想象这会变得多么尴尬。您想要添加一个 addUserToGroup() 函数,该函数理想情况下会根据未找到哪个资源而引发 UserNotFound 或 GroupNotFound 异常。对于子类异常,您将面临一个棘手的决定。使用错误对象上的代码,您就可以做到。
这是路由节点的 fs 模块处理异常。如果您尝试读取一个不存在的文件,它会抛出一个带有一些附加属性的错误实例,例如 code,对于该特定异常,它将设置为 "ENOENT"。
3。返回您的异常。
谁说你必须扔掉它们?在某些情况下,只返回出错的内容可能最有意义。
function readFile(path) {
if (itFailed()) {
return { exCode: 'NotFound' }
} else {
return { data: 'Contents of file' }
}
}
在处理大量异常时,这样的解决方案可能最有意义。这很简单,并且可以帮助自我记录哪些函数给出了哪些异常,这使得代码更加健壮。缺点是它会给你的代码增加很多臃肿。
完整示例:
function readFile(path) {
if (Math.random() > 0.5) {
return { exCode: 'NotFound' }
} else {
return { data: 'Contents of file' }
}
}
function main() {
const { data, exCode } = readFile('./example.txt')
if (exCode === 'NotFound') {
console.log('Could not find the file.')
return
} else if (exCode) {
// We don't know how to handle this exCode, so throw an error
throw new Error(`Unhandled exception when reading file: ${exCode}`)
}
console.log(`Contents of file: ${data}`)
}
main()
非解决方案
其中一些解决方案感觉工作量很大。只是抛出一个对象文字很诱人,例如throw { code: 'NotFound' }。 不要这样做! 堆栈跟踪信息会附加到错误对象上。如果其中一个对象字面量滑过并成为未捕获的异常,则您将无法通过堆栈跟踪来了解它发生的位置或方式。一般来说,调试会困难得多。
奖励:显式例外
当您的包开始处理大量异常时,我建议使用上述“返回异常”方法,以帮助您跟踪哪些异常可能来自哪里(至少在包内部使用它 -你仍然可以为你的包用户扔东西)。有时即使是这样的解决方案也不够好。我整理了一些显式异常包来帮助解决这些场景(可以找到here。还有一个简单的版本,你可以复制粘贴到你的项目here)。这个想法是要求用户明确列出他们希望函数调用提供哪些异常。这使得跟踪异常路径变得非常容易。
try {
// readFile() will give an exception.
// In unwrap(), you list which exceptions you except, to help with self-documentation.
// runtime checks help ensure no other kinds of exceptions slip through.
return unwrap(readFile('./example.txt'), ['NotFound'])
} catch (err) {
// Handle the exception here
}
完整示例:
// You must have the explicit-exceptions package for this code to run
const { Exception, wrap, unwrap } = require('explicit-exceptions')
const readFile = wrap(path => {
throw new Exception('NotFound')
})
function main () {
try {
return unwrap(readFile('./example.txt'), ['NotFound'])
} catch (ex) {
if (!(ex instanceof Exception)) throw ex // Not an exception, don't handle it
console.assert(ex.code === 'NotFound')
console.log('File not found')
}
}
main()