[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)