nullthrows vs. try...catch:两种错误处理哲学的较量

71 阅读3分钟

nullthrows vs. try...catch:两种错误处理哲学的较量

在JavaScript 和 TypeScript 开发中,处理潜在的 nullundefined 值对于编写健壮的代码至关重要。虽然 try...catch 块是一种基础的错误处理机制,但像 nullthrows 这样的断言式函数提供了一种不同的、更主动的方法。让我们通过一个真实世界的代码示例来探讨它们的区别和优势。

代码示例: inject.ts

这是一个来自 Chrome 扩展内容脚本的代码片段。它的工作是将另一个脚本(backend)注入到网页中。

import backend from 'raw-loader!./backend'

function nullthrows<T>(x: T | null | undefined, message?: string): T {
  if (x != null) {
    return x
  }
  const error: any = new Error(
    message !== undefined ? message : 'Got unexpected ' + x
  )
  error.framesToPop = 1 // 跳过 nullthrows 自己的堆栈帧
  throw error
}

function injectCode(code) {
  const script = document.createElement('script')
  script.textContent = code

  // 此脚本在 <head> 元素创建之前运行,
  // 因此我们将其添加到 <html> 元素上。
  nullthrows(document.documentElement).appendChild(script)
  nullthrows(script.parentNode).removeChild(script)
}

injectCode(`;(function(){
  var exports = {};
  ${backend}
})()`)

nullthrows 的方法:防御性断言

上面看到的 nullthrows 函数是“断言函数”或“卫语句”的经典示例。其哲学与 try...catch 有着根本的不同。

nullthrows 的主要优势

  1. 快速失败与早期检测

    • nullthrows 在变量即将被使用的那一刻检查其是否为 nullundefined。如果值无效,它会立即抛出错误,停止执行。这可以防止 null 值在应用程序中进一步传播,从而导致更隐蔽的错误。
    • 在我们的示例中,nullthrows(document.documentElement) 确保我们甚至不会尝试null 对象上调用 .appendChild(script)。而 try...catch 仅在失败的尝试之后才会捕获 TypeError
  2. 更清晰的意图和自文档化代码

    • 使用 nullthrows 向任何阅读代码的人明确地表明:“我,作为开发者,断言此值在此处不能为 null。如果为 null,那就是一个 bug。”
    • 这使得代码的假设变得透明。try...catch 块则更为模糊;它可能正在处理一个预期的运行时错误(如网络请求失败),也可能是一个意外的程序 bug。nullthrows 消除了这种模糊性。
  3. 精确且信息丰富的错误

    • nullthrows 抛出的错误非常具体:“Got unexpected null”。这立即告诉开发人员哪里出了问题。
    • try...catch 可能会捕获一个更通用的 TypeError: Cannot read properties of null (reading 'appendChild')。虽然也是一个错误,但它需要额外的推导步骤才能弄清楚哪个变量是 null
  4. TypeScript 中的类型收窄

    • 对于 TypeScript 用户来说,断言函数提供了一个强大的好处。当一个变量 x 通过 nullthrows(x) 后,TypeScript 编译器就会明白 x 不再是 nullundefined。这将类型从 T | null | undefined“收窄”为 T,从而允许您在不进行进一步检查的情况下访问其属性,并满足严格的空值检查编译器选项。

try...catch:用于可预期的、可恢复的错误

try...catch 仍然是处理可预期的并且程序可以从中恢复的错误的理想工具。这些通常是超出程序员直接控制范围的外部故障。

try...catch 的良好用例:

  • 网络请求 (fetch, axios)
  • 文件系统操作(读/写文件)
  • 解析用户输入或外部数据 (JSON.parse)

总结

根据错误的性质选择您的错误处理工具:

  • 对内部不变量和程序 bug 使用 nullthrows 风格的断言。 将这些视为对代码契约的违反。目标是尽早、响亮、清晰地崩溃,以便于调试。在 inject.ts 示例中,document.documentElement 在有效的浏览器上下文中永远不应为 null,这使其成为断言的完美候选者。
  • 对外部的、运行时的错误使用 try...catch 在这些情况下,您预计会发生故障,并希望优雅地处理它,例如向用户显示消息、重试操作或回退到默认状态。