5 ms·
I don't see why it is a 'lesser evil'. "Explicit and mandatory handling is desirable for most errors." -- that depends on the domain. Quite often all the calle
by protomolecule 2y ago
I don't see why it is a 'lesser evil'.
"Explicit and mandatory handling is desirable for most errors." -- that depends on the domain. Quite often all the caller needs is to cancel whatever it is doing and propagate the error. In this case why do we need a detour into the f() function in your example?
From the part of your essay that you linked to:
"Potentially leaving your data in an inconsistent, half-way modified state." -- this is just not the case with RAII.
"No one wraps every line in a try-catch." -- no one needs to.
- Expurple 2y ago> Quite often all the caller needs is to cancel whatever it is doing and propagate the error. Yes, this is the most common case. That's why Rust has `?` to support it as well as exceptions do. My take is that: - The caller should make this decision explicitly. This avoids propagating (or catching) the error accidentally and makes the code easier to understand and review (see the example with `f(g(x))` vs `f(g(x)?)?`). - Exceptions make the other error handling patterns too unergonomic, and they're not uncommon enough to justify this cost. My post does a bad job at providing more examples to demonstate that they aren't uncommon. But e.g. `.map_err(Wrapper)?` is way more convenient than catching and throwing a wrapper, which isn't so uncommon in Java. > "Potentially leaving your data in an inconsistent, half-way modified state." -- this is just not the case with RAII. RAII solves this only when you have locally-owned data or some locally-owned "scope guard" [1] that acts as a "defer", like std::sync::MutexGuard. This doesn't help when you hold a non-owned &mut to some private data structure, you're in the middle of modifying it and currently it doesn't hold its own invariants because you haven't finished restoring those yet. Now I understand that the post lacks a concrete example to demonstate this. I'm sorry. If you're interested in this topic, you can learn more by googling about exception safery (aka panic safety) in Rust and C++. Both are languages with RAII that doesn't solve the issue completely. [1] https://docs.rs/scopeguard/latest/scopeguard/ https://docs.rs/scopeguard/latest/scopeguard/
- protomolecule 2y ago>The caller should make this decision explicitly. Isn't it too low-level? Why not define instead cleanup/recovery through RAII and occasional try/catch for maintaining tricky invariants, and have no need to manually propagate errors? >`.map_err(Wrapper)?` is way more convenient than catching and throwing a wrapper If you only wrap exceptions on the API boundary it might be more ergonomic than having to manually propagate errors inside the API implementation and map them anyway. >you're in the middle of modifying it and currently it doesn't hold its own invariants because you haven't finished restoring those yet Sometimes it's better to call throwing functions and get needed data before modifying the private data, sometimes it's time to use try/catch. Is it happening often? >Exceptions make the other error handling patterns too unergonomic But so does packing valid data and errors together. Imagine you need to call f(h1()) and f(h2()) where h1() and h2() return the same data type for valid data but different types of errors. What do you do? >the post lacks a concrete example Yes, having a specific example seems to me to be the best way to discuss design tradeoffs, but you did the second best thing -- you started with declaring your assumptions (exceptions are disguised return values and optimizing ergonomics of passing errors through f(g(x)) is important) with both of which I happen to disagree)
- Expurple 2y ago> Sometimes it's better to call throwing functions and get needed data before modifying the private data Haha! Here we go back to the topic of unchecked exceptions and "the lesser evil". With unchecked exceptions, every function is throwing! Even if it shouldn't throw today according to the docs: 1. The docs could be simply wrong. 2. It isn't a semver breaking change to start throwing in a future version. The compiler won't warn you when this happens. You won't manually audit all your dependencies when updating them [1]. We come back to the need to properly reflect errors in the type system. Without returning them by value, the only popular alternative is checked exceptions. But, as I mentioned in the updated version of the post [2], checked exceptions in Java are unfriendly to generic code. This is important and will come up right now: > Imagine you need to call f(h1()) and f(h2()) where h1() and h2() return the same data type for valid data but different types of errors. What do you do? This depends on the context. A general answer: `f` can be generic over the error type. fn f<E>(result: Result<_, E>) If it needs to do something more specific with E, like logging it, it can also have a more specific bound on E: use std::fmt::Display; fn f<E: Display>(result: Result<_, E>) If it has to be a concrete function, it can use runtime polymorphism instead: use std::fmt::Display; fn f(result: Result<_, Box<dyn Display>>) // in this case, the caller has to explicitly box the error f(h1().map_err(Box::new)) --- [1] Unless you operate on a really serious level where you should strive to use a more reliable language anyway. [2] https://home.expurple.me/posts/rust-solves-the-issues-with-exceptions/#checked-exceptions-in-java https://home.expurple.me/posts/rust-solves-the-issues-with-e...