错误不是异常

2025-06-10

错误不是异常

听我在播客中解释这一点

TL;DR

  • 错误是不可恢复的,异常是常见的。
  • 大多数语言(例如 Java、PHP)都内置了这种区别。然而,其他语言(例如 Go)则反过来命名它们。还有一些语言(例如 JavaScript、Python)将它们视为同义词。
  • 无论您如何命名事物,都应该在代码中分别处理错误和异常,否则就会发生不好的事情。

因为我一开始学的是 JS/Python,后来才转到 Go,完全没接触过 Java,所以为了搞清楚这个区别,我花了好几个小时思考和研究。这可不是不言而喻的!

语境

如果您曾经在期望其调用者的throw函数中发现错误那么您做错了catch

好吧,我承认我只是为了吸引眼球而夸大了我的观点。但我对此确实感同身受,所以……

最近,我在浏览 Go FAQ 时想起了这一点,并提醒我Go 没有异常

什么?如果你一直使用一种存在异常的语言进行编码,那么这一点应该会让你大吃一惊。

Go 没有trycatch尽管这些语言结构已经存在了几十年,Go 还是选择了Defer、Panic 和 Recover来代替。按照惯例和设计,Go 强烈地认为错误应该被返回,而不是被抛出

但为什么

依靠异常处理来处理错误要么导致代码复杂,要么导致无法处理的错误。

这种代码在 JavaScript 中很常见:

function trySomethingRisky(str) {
        if (!isValid(str)) throw new Error('invalid string!')
        return "success!"
}

function main() {
    try {
        return trySomethingRisky(prompt('enter valid name'))
    } catch (err) {
        if (err instanceof Error) {
            // handle exceptions
        } else {
            // handle errors
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

如果您认为自己不经常编写这种代码,那么您可能没有充分考虑故障模式。

  • JavaScript 没有原生的方式来指示函数调用时是否会抛出异常。所以你无法在 lint 中对此进行检测——你必须在早期的手动代码审查中或之后的 bug 报告中支付这笔费用。
  • 如果出现错误的字符串,一次无辜的fs.readFileSync调用就可能导致整个服务器崩溃(或内存泄漏描述符)。
  • catch浏览器中没有的 Promise 调用将只记录静默错误(糟糕的用户体验)。

您跨越的功能和模块边界越多,您就越需要考虑防御性地添加try/catch和处理可能发生的各种错误,并且追踪错误开始的位置和处理位置就越困难。

补充一下,像这位 RedditorMatt Warren这样的作者,提出了一个以性能为导向的论点,鼓励开发者不要过度使用异常。异常会涉及到内存和计算密集型的堆栈搜索。这在规模化情况下很重要,但我们大多数人永远不会遇到这种情况,所以我选择不把它当回事。

错误与异常

让我们尝试定义一下:

  • 异常是预期的失败,我们应该从中恢复。
  • 错误是意外的失败。根据定义,我们无法从意外的失败中优雅地恢复。

你可能会注意到一个讽刺的反转——错误才是“例外”,而例外却是常规。这让谦卑的作者非常困惑。

这无疑是因为JavaScriptPython和其他语言将错误和异常视为同义词。因此,Error当我们真正想抛出异常时,却抛出了错误。

PHPJava似乎在语言中已经体现了这种差异。

为了让事情变得更加混乱,Go 使用error其他语言称之为异常的地方,并依赖于panic“抛出”其他语言称之为错误的地方。

Result注意:Chris Krycho 观察到您可以以类似的方式使用 Rust、F# 和 Elm ,以及 Haskell 的Either

异常处理与错误检查

我们需要不同的范例来处理错误和异常,这当然不是什么新鲜事。维基百科“异常处理”条目引用了 Tony Hoare(快速排序、CSP 和空引用的创建者)的话,他认为异常处理“很危险。不要允许这种语言以目前的现状用于可靠性至关重要的应用程序。

这是 1980 年说的话,但 40 年过去了。

异常处理的替代方法是错误检查。

Go 中的错误检查

注意:Go 似乎强烈地认为“错误”是常规操作,而异常(虽然没有正式命名,但在 Go 中使用panic)才是“异常”——这与其他语言截然相反。我选择使用 Go 原生术语——以增加全局混乱为代价,最大限度地减少局部混乱。

错误在 Go 中是值,用于传递,而不是抛出。Go 的 FAQ 值得在此引用:

我们认为,像 try-catch-finally 这样的语法结构,将异常与控制结构耦合会导致代码复杂化。这种做法还会鼓励程序员将太多普通错误(例如无法打开文件)标记为异常。

当出现问题时,您的默认选择应该是使用多值返回来报告错误:

i, err := strconv.Atoi("42")
if err != nil {
    fmt.Printf("couldn't convert number: %v\n", err)
    return
}
fmt.Println("Converted integer:", i)
Enter fullscreen mode Exit fullscreen mode

这种模式同样存在我上面提到的缺点,只不过 Go 会在以下情况下拒绝编译:1)没有在调用处赋值所有返回值;2)没有使用你赋的值。这两条规则结合起来,指导你在错误发生的地方明确处理所有错误。

异常仍然有用——但语言会通过 来提醒你,应该尽量少用它panic()。你仍然可以在 Go 中将recover()其视为后门try/ catch,但你会遭到 Gophers 的质疑。

Node 中的错误检查

JavaScript 缺少我上面提到的强制您处理错误的两个功能。

为了解决这个问题并轻轻地提醒你,Node 使用了错误优先回调

const fs = require('fs');

function errorFirstCallback(err, data) {
  if (err) {
    console.error('There was an error', err);
    return;
  }
  console.log(data);
}

fs.readFile('/some/file/that/does-not-exist', errorFirstCallback);
fs.readFile('/some/file/that/does-exist', errorFirstCallback);
Enter fullscreen mode Exit fullscreen mode

这种模式在大多数 Node 库中都是惯用的,但是我们离 Node 越远,在编写库和应用程序代码时,我们就越容易忘记抛出错误还有其他方法。

最后,这些回调很诱人promisify

const util = require('util');
const fs = require('fs');

const stat = util.promisify(fs.stat); // i am using fs.stat here, but could be any error-first-callback userland function

// assuming top-level await
try {
    const stats = await stat('.')
    // do something with stats
} catch (err) {
    // handle errors
}
Enter fullscreen mode Exit fullscreen mode

我们又回到了原点——能够任意地将错误和异常抛到高处,并且必须在同一个地方处理它们。

其他读物

感谢Charlie YouRobin Cussol审阅本文草稿。

鏂囩珷鏉ユ簮锛�https://dev.to/swyx/errors-are-not-exceptional-1g0b
PREV
如何设计几乎任何后端并将其部署到 AWS(无需代码)
NEXT
顶级 AI 搜索引擎可提高生产力 AWS 安全 LIVE!