[HN Gopher] A guide to error handling in Rust
       ___________________________________________________________________
        
       A guide to error handling in Rust
        
       Author : nrc
       Score  : 157 points
       Date   : 2022-11-09 09:29 UTC (13 hours ago)
        
 (HTM) web link (nrc.github.io)
 (TXT) w3m dump (nrc.github.io)
        
       | MontagFTB wrote:
       | An aside: what's the template that gets articles formatted this
       | way in GitHub pages? I found it very appealing.
        
         | mrec wrote:
         | This is `mdbook` - https://rust-lang.github.io/mdBook/
        
           | MontagFTB wrote:
           | Thanks!
        
       | sposeray wrote:
        
       | tialaramex wrote:
       | I feel like this document makes the Try operator (?) and its
       | associated trait more mysterious than necessary. Most people
       | probably won't need to implement Try, especially before it is
       | stabilised, but it's not that much more complicated than say,
       | AddAssign the trait which you implement to make the Add
       | Assignment (+=) operator work on your type.
       | 
       | The key trick of Try is that it converts something (by default an
       | Option or a Result or async Polls of those types) into a
       | ControlFlow+. This is the one nice trick about Exceptions in
       | languages which have them - they influence control flow, but Rust
       | reified it as a vocabulary type which I think is much better. We
       | can pass this thing back to somebody who _cares_ about the
       | resulting control flow, not just suddenly wrench the control flow
       | out from under the rest of the software.
       | 
       | + unlike Try, ControlFlow is actually a stable type you can use
       | today in your Rust and, like std::cmp::Ordering it's useful even
       | just as a vocabulary type, disregarding its semantics. Library A
       | and Library B, written by different people, in different
       | circumstances, both agree that ControlFlow::Continue is
       | _continue_ and ControlFlow::Break is _break_ whereas who knows
       | what the boolean false from Library A means to Library B, let
       | alone what if anything Library B 's custom type BPartialResult
       | means to Library A's code.
        
         | petertodd wrote:
         | BTW if you have read about the `Try` trait before and are
         | wondering what `ControlFlow` is, read it again:
         | https://doc.rust-lang.org/nightly/std/ops/trait.Try.html
         | 
         | `Try` was recently changed significantly with the introduction
         | of `ControlFlow`. IMO it's a big improvement.
        
         | mmastrac wrote:
         | TIL what ControlFlow is. This is super interesting and solves
         | some problems I thought were impossible.
         | 
         | Error handling has gone from uber painful in 2018 to pretty
         | decent in the latest editions.
        
         | gwbas1c wrote:
         | I do some hobby projects in Rust.
         | 
         | One gotcha that I hit was using ? in sample code in
         | documentation. It didn't work, so I had to replace all of my ?
         | with .unwrap().
         | 
         | I generally consider .unwrap() a poor example, because it
         | encourages writing code that could crash a program
         | unnecessarily.
        
           | erk__ wrote:
           | You can use it in sample code in the documentation, but you
           | will need to add a bit of boilerplate around it:
           | https://doc.rust-lang.org/rustdoc/write-
           | documentation/docume...
        
           | cercatrova wrote:
           | You can set lints for cargo, for example to warn or even
           | disallow compiling with any `unwraps` or `expect`s. I use
           | cargo-cranky which makes using lints super easy, cargo
           | doesn't yet have native functionality to set which lints
           | should be enabled or disabled.
        
       | oxff wrote:
       | It is perhaps too verbose by default, as indicated by popularity
       | of thiserror and anyhow crates.
        
         | Ciantic wrote:
         | It can be as verbose as you want. This guide as well as others
         | suggests using concrete error types for libraries but anyhow
         | "catch all" method for applications.
         | 
         | There is disadvantages for going "catch all" libraries as then
         | you can't be sure you are catching all errors.
        
         | exDM69 wrote:
         | Perhaps, but it's also much better than it was two years ago
         | and there is work going on to make it better two years in the
         | future.
         | 
         | A myriad of experimental prototypes (like the failure crate and
         | its descendants) have been made, experimented with and then
         | retired and looks like the progress is converging to these two
         | complementary error handling crates (anyhow, thiserror, and a
         | few mostly-compatible variants like eyre), and work going on to
         | standardize some aspects of it so (parts of) these crates can
         | be retired. There's also core::error that's bringing this to
         | no-std environments.
         | 
         | So yeah, it definitely was not great on day 1 and there's been
         | a lot of churn on error handling but it is going in the right
         | direction.
        
         | nrc wrote:
         | Many of the features of such crates are making their way to the
         | standard library, so things will definitely improve. Figuring
         | out what is best has taken some time and Rust has not wanted to
         | prematurely commit.
        
           | oxff wrote:
           | I like what the error handling achieves, it is actually
           | readable way to understand the divergent control flow paths
           | (and probably majority of code is read more than written),
           | but I do not enjoy writing the initial boilerplate, so that's
           | good to hear.
        
           | kibwen wrote:
           | If anyone's interested in helping to shape the future of
           | Rust's built-in error-handling story, there's an error
           | handling project group that's been doing great work recently,
           | e.g. the major effort to move the Error trait into libcore (
           | https://github.com/rust-lang/project-error-handling/issues/3
           | ) and stabilizing std::backtrace. You can follow along or get
           | involved via the #project-error-handling channel on the Rust
           | zulip: https://rust-lang.zulipchat.com/
        
       | anacrolix wrote:
       | try blocks? is this a bit out of date?
        
         | anacrolix wrote:
         | I have indeed confused it with try! macro
        
         | nrc wrote:
         | Nope, try blocks are a new (unstable) feature
        
         | Spiritus wrote:
         | I think you're mixing up try blocks with the old try! macro.
        
         | tialaramex wrote:
         | How so? As it says this is not yet a stable feature, but if you
         | run nightly Rust it's available.
         | 
         | Try blocks let you do what ? (the Try operator) does within a
         | block, rather than needing to split out a separate function for
         | it, which makes sense because why should functions be special
         | in this way?
        
       | svnpenn wrote:
       | This glaring omission from this is the "enum idiom":
       | 
       | https://doc.rust-lang.org/std/convert/trait.From.html#exampl...
       | 
       | they talk about it here:
       | 
       | https://nrc.github.io/error-docs/error-design/error-type-des...
       | 
       | but including more than a snippet would go a long way to that
       | "aha" moment I think. This was frustrating for me browsing this
       | site. The author wrote 10 pages of docs, but nearly all the
       | examples are like 5 line snippets of code. I think examples are
       | equally important as the discussion itself. Rust itself suffers
       | from the same problem:
       | 
       | https://github.com/rust-lang/book/issues/3348
        
         | epage wrote:
         | Whats the "aha" moment for it, the Froms?
         | 
         | The author might not have included that as they call out you
         | likely shouldn't directly wrap another error.
         | 
         | I go a step further and think that public errors shouldn't have
         | From's for concrete types, exposing your implementation
         | details, and that enum errors are more generally too tied to
         | implementation details to be used in libraries.
        
           | shepmaster wrote:
           | > public errors shouldn't have From's for concrete types
           | 
           | > enum errors are more generally too tied to implementation
           | details to be used in libraries
           | 
           | I generally agree. SNAFU addresses these problems in two
           | ways:
           | 
           | 1. The `From` implementation is not created for the
           | underlying error but for an intermediate type (by default).
           | That type is private to the crate (by default) and cannot
           | expose implementation details.
           | 
           | 2. There's an opaque error facility to completely hide the
           | enum details.
           | 
           | Put together, that looks something like...
           | use snafu::prelude::*;         use std::{             fs,
           | path::{Path, PathBuf},         };
           | #[derive(Debug, Snafu)]         enum ErrorImpl {
           | #[snafu(display("Could not read the config file {}",
           | path.display()))]             UnableToReadConfig {
           | source: std::io::Error,                 path: PathBuf,
           | },                      #[snafu(display("Could not write the
           | config file {}", path.display()))]
           | UnableToWriteConfig {                 source: std::io::Error,
           | path: PathBuf,             },         }
           | #[derive(Debug, Snafu)]         pub struct Error(ErrorImpl);
           | pub type Result<T, E = Error> = std::result::Result<T, E>;
           | pub fn do_stuff_with_config(path: &Path) -> Result<()> {
           | let config =
           | fs::read_to_string(path).context(UnableToReadConfigSnafu {
           | path })?;             fs::write(path,
           | config).context(UnableToWriteConfigSnafu { path })?;
           | Ok(())         }
           | 
           | Other things about SNAFU:
           | 
           | - It's very easy to add valuable context to the errors. See
           | how the `&Path` context is transformed to a `PathBuf` with
           | low ceremony in the example.
           | 
           | - You can create struct- or enum-based errors.
           | 
           | - You can use "stringly-typed" errors (akin to anyhow) but in
           | combination with strongly-typed errors. This allows you to
           | start out with a loose error handling regimen and make it
           | stronger as you go along.
           | 
           | - There's support for capturing backtraces or lightweight
           | file/line/column information.
           | 
           | - There's a pretty error reporter for usage with `main`
           | functions or tests.
           | 
           | - There's support for the nightly-only Provider API.
        
           | svnpenn wrote:
           | OK, but what do you do then? Its not really helpful to say
           | "this bad", if you don't offer a "this good". Of the maybe 10
           | approaches I have seen to Rust error handling (including
           | using external crates, gross), the "enum idiom" is the most
           | elegant and flexible to me, and coming from another language
           | feels the most natural.
        
       | valenterry wrote:
       | Too bad Rust doesn't have union types (aka adhoc / anonymous
       | unions) yet.
       | 
       | Without them, using typed errors is very clumsy. Optimally, you
       | would write the following code:                   fn foo(r1:
       | Result<i32, Error1>, r: Result<i32, Error2>) {             let i1
       | = r1?;             let i2 = r2?;             // ...         }
       | 
       | and Rust would infer the return type to be Result<String, Error1
       | | Error2> without having to do any extra definitions or
       | conversions.
        
         | jamincan wrote:
         | The library ecosystem does fill that gap somewhat with crates
         | like anyhow [1] which also allow you to add context to the
         | errors you return.
         | 
         | 1. https://docs.rs/anyhow/latest/anyhow/
        
           | valenterry wrote:
           | Unfortunately this does not solve the problem. If you use
           | this, you lose typesafety and the documentation you get
           | through the (inferred) types. I.e. with union types you can
           | see at a glance in your IDE which types of errors a function
           | can return - this is too valuable to give it up.
        
             | zaarn wrote:
             | You don't really loose type safety, rather it moves to
             | runtime. Downcasting an anyhow error to the concrete
             | internal type is certainly possible and won't panic in my
             | experience.
        
               | valenterry wrote:
               | "moves to runtime" exactly means to lose typesafety
               | though.
        
         | TotempaaltJ wrote:
         | For anyone interested in what this would look like in Rust now,
         | there's two ways. For libraries, people tend to recommend the
         | thiserror crate. Code sample[0]:
         | #[derive(thiserror::Error, Debug)]         enum Error {
         | #[error("One")]             One(#[from] Error1),
         | #[error("Two")]             Two(#[from] Error2),         }
         | fn foo(r1: Result<i32, Error1>, r2: Result<i32, Error2>) ->
         | Result<..., Error> {             let i1 = r1?;             let
         | i2 = r2?;             // ...         }
         | 
         | Whereas for binaries, people usually recommend anyhow. Code
         | sample[1]:                   fn foo(r1: Result<i32, Error1>,
         | r2: Result<i32, Error2>) -> anyhow::Result<...> {
         | let i1 = r1?;             let i2 = r2?;             // ...
         | }
         | 
         | [0]: https://play.rust-
         | lang.org/?version=stable&mode=debug&editio... [1]:
         | https://play.rust-lang.org/?version=stable&mode=debug&editio...
        
           | exDM69 wrote:
           | For simple tasks you can get away without any external crates
           | by using `Result<T, Box<dyn Error>>`. But it's much more
           | comfortable to use thiserror or anyhow in the long run.
           | fn read_string() -> std::io::Result<String> {
           | Ok("123".to_owned())         }              fn main() ->
           | Result<(), Box<dyn std::error::Error>> {             let s =
           | read_string()?; // io::Error             let n =
           | i32::from_str_radix(&s, 10)?; // num::ParseIntError
           | println!("read number: {n}");             Ok(())         }
        
             | xedrac wrote:
             | Except you often need `Result<T, Box<dyn Error + Send +
             | 'static>>` if you go that route. At the very least, you
             | should create a type alias for it. I very much prefer the
             | use of `anyhow` and/or `thiserror` depending on if I need
             | typed errors.
        
               | WirelessGigabit wrote:
               | And the reason it works in anyhow is ... They have those
               | conversions: https://docs.rs/anyhow/latest/anyhow/struct.
               | Error.html#impl-...
        
           | ithkuil wrote:
           | what is the TL;DR difference between anyhow and Box<dyn
           | Error> ?
        
             | mcronce wrote:
             | It's detailed here -
             | https://docs.rs/anyhow/1.0.66/anyhow/struct.Error.html -
             | the list is short but IMO significant
        
           | icambron wrote:
           | The binary vs library thing seems like an oversimplification
           | to me. I think it's more like: do you need callers to handle
           | this error specifically? With a library the answer is "I
           | don't know, better let them do it", so you don't want anyhow.
           | But in a binary, you may or may not, and it depends on the
           | error.
           | 
           | The pattern I use in my app is to use thiserror, and then
           | just have an anyhow catch-all. That lets me do specific stuff
           | where I know I'm going to need specific handling, and an
           | easy-to-use fallback for just saying "this bad thing
           | happened" with the anyhow! macro.
           | #[derive(Debug, thiserror::Error)]         pub enum Error {
           | #[error("Not logged in")]             NotLoggedIn,
           | #[error(transparent)]             Api(#[from] ApiError),
           | // etc                  #[error(transparent)]
           | Other(#[from] anyhow::Error),         }
           | 
           | I don't know if this is the best pattern but it's worked
           | really well for me.
        
         | tialaramex wrote:
         | Even if the "Ad hoc union" becomes a thing in Rust, you are not
         | likely to get inference of return types.
         | 
         | The return type is part of the function signature and Rust
         | deliberately doesn't infer signatures, in languages with "too
         | much" inference it's impractical for the human programmer to
         | keep track of types because it's all inferred, this has started
         | to be a problem in C++ as more and more things are auto. Rust
         | has some very sophisticated inference _inside_ a function
         | (including partial inference and inferring types from how they
         | 're later used), but none for the signature.
        
           | valenterry wrote:
           | I think it's the job of the IDE to make that work, but I
           | agree, without IDE this can quickly become a problem.
        
             | friendzis wrote:
             | I think method signatures are part of
             | application/typesystem _design_ and should not be inferred.
             | Explicitly provided types are a feature. Inferred /auto
             | type signatures are "necessariy evil" to reduce boilerplate
             | type declarations around code.
             | 
             | While codeblocks `fn1(fn2(), fn3)` and `var r1 = fn2(); var
             | r2 = fn3; fn1(r1, r2)` are more or less identical, unless
             | you have static type definitions for these methods you
             | start having a very bad time inferring what _types_ are
             | being passed around.
             | 
             | Consider typical python wrapper library with liberal use of
             | *kwargs to pass non-wrapped arguments down to wrappee.
             | Those arguments and their types (as much as they are
             | available in python, you get the idea) are entirely missing
             | from wrapper code and make changes at call site pretty
             | difficult
        
               | valenterry wrote:
               | > unless you have static type definitions for these
               | methods you start having a very bad time inferring what
               | types are being passed around.
               | 
               | It doesn't matter if the types are annotated explicitly
               | or inferred. The amount of information is 100% the same.
               | The IDE could just fill in the types _exactly_ in the
               | same way as they would look like when annotated by hand -
               | maybe just with a different color.
               | 
               | IntelliJ does this quite well, see for instance:
               | https://i.stack.imgur.com/tiqjc.png
               | 
               | This is not a guess. This is the real types. There is
               | literally no difference in the behaviour/semantics of the
               | code.
               | 
               | > Consider typical python wrapper library with liberal
               | 
               | No, because python is not statically typed. You can't
               | compare that.
        
               | dllthomas wrote:
               | > It doesn't matter if the types are annotated explicitly
               | or inferred. The amount of information is 100% the same.
               | 
               | Explicit offers the opportunity for narrowing, comments,
               | and sometimes choice of names. Otherwise, yes.
        
               | wtetzner wrote:
               | I like OCaml's approach, where you write types in module
               | signatures, but don't need to put them in the
               | implementation.
        
           | dllthomas wrote:
           | While I don't strongly object to Rust's choice here, and I
           | agree that production code should have a type signature on
           | every function, I think this is more a place for
           | lint/clippy/whatever. There's no need to gate the programmer
           | trying something on them having produced a type signature
           | that could be inferred.
        
         | xavxav wrote:
         | I know that this is a common wish, but anonymous sum types have
         | pretty catastrophic impacts on type checking and lead to all
         | sorts of bizarre corner cases like the following:
         | let a = if cond {         1        } else {         1.0
         | };              a + 3
         | 
         | Now, the error would be pushed to the `+` operator because
         | there isn't an `Add` for `f32 | u32`. Granted, this is a
         | trivial example, and a programmer can easily see through it,
         | but in general this can get very overwhelming and cause errors
         | to leave their 'root cause'.
        
           | valenterry wrote:
           | > Now, the error would be pushed to the `+` operator because
           | there isn't an `Add` for `f32 | u32`.
           | 
           | Why not? It makes total sense for one to exist. This is
           | something the language needs to deal with, not the
           | programmer. But the programmer always sprinkle annotations so
           | that errors can only reach so far.
        
             | dllthomas wrote:
             | I think the ramifications of `Add` (and friends) for union
             | types is interesting, but I think we can imagine
             | alternatives that are clearly mistakes and so it seems like
             | you're missing the point (... is my guess about the
             | downvotes). Your last sentence makes an important point,
             | though - relying on type inference always lets type errors
             | propagate further than they would if everything was
             | explicitly typed. Adding more annotations constrains that,
             | although whether that would be sufficient is a more
             | complicated discussion that's probably quite sensitive to
             | the particulars of a given language.
        
             | shakow wrote:
             | > It makes total sense for one to exist.
             | 
             | I disagree: IME, when you add a float and an integer, you
             | want to cast float to integer 50% of the time, and integer
             | to float the remaining 50% of the time.
             | 
             | Even if it leads to more verbosity, I prefer arithmetic
             | operations to be endomorphisms and use explicit casts.
        
               | valenterry wrote:
               | I'm sure you can just use a linter or configure the
               | compiler to error out in such a case if there are really
               | no use-cases, no?
        
               | hn92726819 wrote:
               | What do you mean? It already errors out if you try adding
               | them together because it requires explicit casts. What
               | the comment you're replying to is saying is that it's
               | better to just explicitly cast than figure out what the
               | compiler guesses.                   let x=19/10+0.5
               | 
               | I want to decide for myself if my result is 2.4, 1.5, 1,
               | or 2 in the example above
        
               | valenterry wrote:
               | The OP doesn't like that the error moves from the
               | condition to the addition. And what I mean is that then
               | just lint that there shouldn't be unions of number-types
               | or so.
        
           | karmakaze wrote:
           | Typescript seems to have no problem with it (nor do I suspect
           | F#/OCaml):                 class A {         x = 10;
           | scale(n: number): void {           this.x *= n;         }
           | }       class B {         y = 10;         scale(n: number):
           | void {           this.y *= n;         }       }         var
           | z: A | B;       z = new A       z.scale(2);       z = new B
           | z.scale(2);
           | 
           | I was also looking for Sum Types/anonymous enums[0].
           | 
           | [0] https://news.ycombinator.com/threads?id=karmakaze&next=33
           | 505...
        
           | dllthomas wrote:
           | One option might be to differentiate syntactically between
           | branches of an if/match that are allowed to expand their type
           | (to a union or to a bigger union) and those that are not. I
           | am not sure how far that generalizes, though.
        
         | yakubin wrote:
         | ,,Yet"? Are there any plans to add them?
        
           | kibwen wrote:
           | There are not. I don't believe they've ever been thoroughly
           | proposed.
        
         | insanitybit wrote:
         | Anonymous sum types are something I want for error handling as
         | well. In practice though I'm not sure it would really make my
         | life that much better.
        
           | masklinn wrote:
           | It would make life a bit terser, but I'd rather have
           | polymorphic variants. And maybe only anonymous enums over
           | polymorphic variants.
           | 
           | That would make precise error handling on libraries quite a
           | bit better.
        
           | valenterry wrote:
           | Not sum types. Those are union types. The difference is
           | important, since if you work with two results (or two
           | functions that return results) that use the same error-type
           | you most often don't want to end up with a tuple of two times
           | the same error but simply A<String, Error>.
           | 
           | Of course, if you care about which error is from which
           | function, you can always easily do that by wrapping them into
           | a sumtype, but in practice this is a rather rare use-case in
           | application code at least.
        
             | insanitybit wrote:
             | Unions don't have a discriminant. Anonymous Sum types have
             | a discriminant, you just can't name it. Unions in Rust are
             | unsafe because you can't tell what the underlying value
             | will be.
        
               | valenterry wrote:
               | Well, that depends. If the union consists of two types
               | that share the same underlying structure, then obviously
               | at runtime we can never know what the value is.
               | 
               | But otherwise we can. And this is something that we will
               | know at compile-time, so we can prevent runtime-checks
               | that would not work.
        
               | insanitybit wrote:
               | I must not be understanding what you're asking for
               | because it sounds like anonymous sum types.
        
               | valenterry wrote:
               | Maybe this is just about terminology.
               | 
               | But essentially, when it comes to union types, they
               | behave like sets. The compiler merges them. (A | B) | (A
               | | B) is the same as A | B. But for sum types (even
               | anonymous ones such as tuples) the compiler can't merge
               | them because that would lose information (if the result
               | is from the first A | B or the second one). Instead, you
               | end up with a nested structure.
               | 
               | Which one is desired depends on the use-case, but it's
               | definitely different.
        
               | wtetzner wrote:
               | The problem is that if you have e.g.
               | let x: u64|i32 = ...
               | 
               | It's impossible for the compiler to do anything with x
               | without adding some kind of runtime type tag. The
               | representations of those types are different.
               | 
               | But with something like OCaml's polymorphic variants, you
               | could do e.g.                   let x:
               | Big(u64)|Small(i32) = ...
               | 
               | and then switch on the tag (Big or Small) to determine
               | what to do with the values.
        
               | dllthomas wrote:
               | IIUC this kind of union would discriminate only on type.
        
             | proto_lambda wrote:
             | How would that work in a memory safe language? Rust does
             | have (named) untagged unions already (using the `union`
             | keyword), but they are unsafe to use because there is no
             | way to know statically which of the possible variants a
             | given value contains.
        
               | valenterry wrote:
               | You can obviously only call common methods or have to
               | pattern match later and have a way to tell them apart. If
               | you can't tell them apart, the compiler will tell you and
               | you need to tag them somehow.
        
               | proto_lambda wrote:
               | > You can obviously only call common methods
               | 
               | That sounds like trait objects/dynamic dispatch/`dyn`,
               | which comes with runtime costs.
               | 
               | > or have to pattern match later and have a way to tell
               | them apart.
               | 
               | That "way to tell them apart" is a tag, which would make
               | it a tagged union/enum, not an untagged union. Those
               | already exist, though not in an anonymous flavour.
        
               | valenterry wrote:
               | It does, but the developer decides. Also, errors are
               | often propagated and only matched in exceptional cases,
               | so I don't think the impact would be big.
               | 
               | > That "way to tell them apart" is a tag, which would
               | make it a tagged union/enum, not an untagged union.
               | 
               | No. The difference is that a tagged union (sum type) is
               | defined in advance but generally a union is not
               | necessarily tagged but _can_ be tagged.
               | 
               | Example: say you have tagged union with 3 different types
               | / tags A, B and C. You can now define an adhoc/untagged
               | union that is A | C. That means, we can guarantee at
               | compile time, that we will be able to tell A and C apart
               | later. But it is still not the same, because the
               | combination of A and C was decided adhoc and was not
               | predefined by the developer anywhere necessarily - which
               | is what makes it different from A, B, C which where
               | specifically defined by the developer.
        
       ___________________________________________________________________
       (page generated 2022-11-09 23:02 UTC)