Index


Thu Oct 23 19:22:39 CST 2025

Rust 错误处理库调查

https://github.com/zkat/miette

感觉 miette 不错……似乎是 anyhow 的上位替代

Sun Nov 9 19:44:54 CST 2025

https://www.zhihu.com/question/653774450/answer/39270831919:

    正确答案是使用 unwrap, unwind
    , 只要你用 result, 那就是不需要定位发生错误的位置
    我发现好多人分不清错误和异常, 当然可能和某些语言里异常当错误用有关系
    错误就是别人的锅, 输入有问题, 别人处理的有问题, 编译期已经预见到的问题, 此时应该用 Result
    异常就是自己的锅, 自己逻辑没处理好, 代码有 bug, 出问题直接捅破天, 那就用 panic
    显然你用了 Result 就是说明我自己没问题
    既然我自己代码没问题, 我逻辑没问题, 那我干嘛还要追溯我的问题出在哪?
    要有问题那也是输入不对, 别人的库处理的不对, 至于哪里不对, 我放在 Err 里你自己去读吧.
    你要是全盘 Result, 你就成了不粘锅, 输入啥你都说你没问题, 别人查了一圈, 自己也没问题, 希望你好好查下自己有没有逻辑错误
    你就傻了, 赶紧上知乎问 Result 使用 "?" 多次传播错误后,怎么定位最开始发生error的地方?

hmm,有道理的。这是指错误处理应该和追踪分开吗?

好像也不是。这应该只限于无副作用的代码吧。有副作用的代码需要依赖外部环境,要是这种也出错就 panic 也太极端了。

哎那我 panic abort 不就用不了了,太坏了。

看来怎么处理由外部,比如 HTTP 请求和文件系统导致的问题是最难的。

    不过有一说一,又要区分错误类型,又要从最内层把调用栈调试信息往外传,这往往给人一种眼高手低的感觉。
    因为一般来说像 Rust
    这种级别的语言,好的模块封装是很严密的,错误调试不应该深入到模块内部(在 release build 里你甚至深入不进去,因为太多函数直接被内联了,根本看不到出错在哪一行)
    每一个错误类型,在模块的文档上都应该解释清楚它对应什么情况——模块设计者明知道可能产生、可以解释的错误才会浮现到 API 签名上;设计者都不清楚、更不能指望下游调用方处理的错误类型,本来就是一种设计缺陷,就应该 panic 然后设法规避,而不是和稀泥允许它作为一种合法的情况传出模块,毫无意义地污染每一层函数的 Result 签名
    。
    而模块外部的胶水业务代码,关心的往往只在于「调用了模块的哪个 API 产生的错误」(这样就可以查阅模块文档)而不是「错误源头定位到模块内哪个函数的第几行」。
    对于这种需求,模块内用 thiserror 定义有区分度的错误类型,模块外要么当场分类处理错误,不然就 将错误转换为带有 backtrace 的 anyhow::Error 向上传递一把梭足矣。

好像也有点道理。

https://www.zhihu.com/question/584713844/answer/2952891793

    C的缺点,就是它本身的语法检查不够严格、智能,甚至它本身都算不上强类型。这就使得它的assert功能薄弱、使得很多错误难以检测;但只要你别作死、别用“容错性代码/防御性代码”把错误混进异常,这些错误还是比较容易通过测试发现的——因此它可以胜任几乎所有支柱性项目。

https://www.zhihu.com/question/425726667/answer/1531780075

    这两种开发模式不同会造成任何单一的错误模型很难同时兼顾二者。如果对错误处理要求的很严格,就意味着无法“凑合处理”;反过来如果错误模型很利于快速/忽略处理,就意味着程序员很容易忽略某段代码可能产生的错误。编译器无法有效的引导程序员提高程序的正确性。这种问题可能通过其他手段变相的改进(比如使用静态代码扫描),但总是没有语言built-in用起来自然和舒适。就算是Java这种同时提供了Checked Exception和Unchecked Exception两种机制的语言也总会引发各种争吵。

    隔三差五就会有人有这种想法,希望当你遇到要往一个十几层深的调用深度的函数里面加一个新的可能产生异常的调用,如果产生异常需要在最外层处理的时候,修改十几层的接口声明、再跟着给每个接口修改了二十个不同的实现之后能让你清醒一点。
异常存在的意义就是用来干脏活。一个接口理应只跟功能相关而跟实现无关,而异常通常都是跟实现相关的,在具体实现之前无法预测可能会出现什么异常,因而将异常作为接口定义的一部分是非常滑稽的,要么需要频繁修改接口,要么所有接口都适配通用异常,CheckedException
就是这么废物的功能,和类型返回值表示异常的情况同理。Golang的Error类型返回值也相当于通用异常,很智障。

    一个 Web 后端应用,本来就到处都有可能出问题,数据库可能连不上,某个地方网络可能堵了,依赖的某个服务可能挂了,轻轻松松就可能给你整出来几十上百个错误——但是如果你只是在编写一个用于计算某笔交易手续费的函数,你压根就没办法也不应该处理这些错误,此时你该怎么办呢?最好的办法就是往上抛异常,直到遇到一个能够处理异常的函数。这也是很多 Spring 应用的做法,搞一个顶层的集中处理异常的单例,碰到能处理的异常就处理,不能处理就包装一下直接作为响应体。
    但是对于一个系统级应用,这种暴力直接把异常往上抛出的方式就显得不那么妥当。系统级应用里只有两种异常——能处理的异常我就一定要考虑怎么正确处理它,不能处理的异常我就得让应用直接崩溃。比如一个数据库应用,碰到硬盘空间不足,我能怎么办呢?先把事务处理完,然后直接停掉整个应用了事,把错误往上抛出是毫无意义的事情,又不可能有哪个函数有办法处理这种问题。又比如一个底层的做数值计算的库,能发生几种错误掰着手指头都能数清楚,而运算过程中又涉及很多脆弱的中间状态,此时把异常往上抛显然不合适,你得认真处理每一种可能发生的错误,并确保仅用于中间计算步骤的临时值不会意外泄漏到上层去,防止输出结果不正确。

有道理的。web 要是也好好处理错误的话那错误数量要爆炸了。

    但是 Unchecked exception 用来写系统级应用不舒服,异常不反映在类型定义上,你得自己确保捕获了所有可能的异常,然后祈祷不会有个不知道哪里冒出来的异常坏了你的好事——或者自己写代码时完全禁用异常,采用自己手搓的一套类似 Result 的机制;而 Result/Either 这种直接把异常写在类型里的做法,写上层应用又很痛苦,比如 Rust 里你得定义数不清的 enum 把各种错误包起来(而给这些 enum 命名又是很头疼的事情),或者用个丑陋的深度嵌套 Result 偷懒。

可以 ai 命名吗。坏了感觉被神秘 ai 爱好者产经带坏了。

    比如在 Rust 中开发上层应用时经常用 anyhow 来处理异常。它的逻辑很简单,就是把所有 Result<T, E> 变成一个 Result<T, anyhow::Error>,还允许用 .with_context 附加一些上下文信息——听上去简直像历史的倒退,怎么又退化成 Unchecked exception 了。但如果你正在开发一个 Web 后端这样的应用,会发觉 anyhow 确实是个很合适的选择,大大减轻了定义一大堆用作错误类型的 enum 并绞尽脑汁给它们起名的负担。

确实哦,anyhow 有点像异常。但是没有栈回溯。不对其实是有的!那 anyhow 感觉是对的,写 web 的时候。

    说到底,不同应用场景对异常处理的需求就是不同的,没有什么优劣之分,甚至不同的异常处理方式之间是可以转换和共存的。真到写代码的时候,该用啥用啥,想爽就用 Unchecked exception,想严谨点就用 ADT 去建模,没什么好坏的说法。

有道理。

    这也就意味着你一个没想好, 底层没用 Result, 后面突然发现其实有可能出错, 现在想用 Result 了。
    恭喜你,现在请你把中间调用的几百个函数签名全改成 Result!

草,绷不住了。这咋办啊。

哎,后面又开始吹代数效应。感觉用起来实则没那么好用……唯一的优点疑似就是依赖注入比较方便。

https://joeduffyblog.com/2016/02/07/the-error-model/

https://zhuanlan.zhihu.com/p/55835404

很多人推荐的文章。试吃一口。

    总的来说,我们的解决方法是同时提供两套错误模型。一方面,对于程序的 Bug,我们提供了快速失败(Fail-Fast)模型,在 Midori 中我们称其为放弃(Abandonment);另一方面,对于可恢复的错误,我们也提供了静态受检查异常(Statically Checked Exception)。这两种错误模型从编程模式到背后的机制都截然不同。放弃会无条件地立即终止整个进程,不会再运行任何用户代码(需要说明的是:一个典型的 Midori 程序会由很多个小的、轻量级的进程组成);异常则允许恢复用户代码的执行,在这个过程中类型系统会起到重要的检查和验证的作用。

    异常系统的另一个问题在于它鼓励粗粒度地处理错误。很多人喜欢返回错误码的方案,就是因为调用一个函数后就必须进行错误处理。(我也喜欢这一点。)而在异常处理系统中,人们经常用一个的try/catch块包住一大坨代码,而不是小心地去应对每一处可能出现的错误。这样的代码很脆弱,而且几乎都是错的;就算现在看起来没问题,将来等代码一重构,问题就会暴露出来。这个问题很大程度上是因为语言没有提供合适的语法导致的。

        可恢复错误通常都是程序化数据验证的结果。有时候程序会检查这个世界的状态,但认为当前的状态不能接受——比如解析一些标签语言、解析用户在网站上的输入、或是网络不稳定等等。在这些情况下,程序应该恢复执行。写程序的开发人员必须提前安排好要怎么处理这些情况,因为不论我们怎么做,都避免不了这些状况。程序给出的响应可能是给用户返回一些信息、重试或者直接放弃这个操作。虽然它们也被称作“错误”,但这些错误都是可预测的,而且对于这些状况,程序通常是有准备的。

    Bug 是一类程序员无法预测的错误。用户输入没有被正确地验证,或是逻辑写错了,等等。这类问题一旦发生,就会极大地损害程序状态;然而它们有时候很隐蔽,甚至要等到它们让别处的代码出现了问题后,才会被间接发现。由于程序员并没有想到会发生这些情况,我们就前功尽弃了。那段有问题的代码所能修改的所有的数据结构都可能已经损坏了,同时因为这些问题没有被直接检测到,很大可能所有的东西都已经出了问题。根据你使用的语言所提供的隔离性,也许整个进程都已经被污染了。

哦哦,这下懂了。网络不稳定属于是可恢复错误。

Tue Nov 11 17:21:25 CST 2025

    合约最基本的形式就是方法的前置条件,它声明了要调用这个方法所必须满足的条件。通常来说先决条件会用来验证参数。它有时也会用来验证目标对象的状态,不过这非常少见,因为对于程序员来说考虑清楚模态(modality)是一件很困难的事。前置条件基本上就是调用者对被调用者做出的承诺。

    void Register(string name)
        requires !string.IsEmpty(name) {
        // 继续执行,字符串一定不为空
    }

这个似乎类型系统也能部分做到,不过因为是在编译期完成的,没有那么灵活。之前学命令式计算的时候处理不变量也是用的类似的机制。有道理的。

    这里的想法是,对于每种情况,当错误发生时,相关的输入都会提供给看守人。看守人随后会执行一些操作——也许是异步的——来进行恢复。很多时候,看守人可以选择返回一个新的参数来执行这项操作。例如,InsufficientPrivileges可以返回一个替代的Credentials来使用。(程序可以向用户弹出一个提示框,让用户切换到有写权限的账户。)对于上面列举的每一种情况,如果看守人不想处理的话,也可以抛出异常,不过这个功能不是看守人模式必须提供的。

感觉有点像 algebra effect