
Rust: The Error Handling Project Group - Santosh83
https://blog.rust-lang.org/inside-rust/2020/09/18/error-handling-wg-announcement.html
======
rkwz
For beginners who are struggling with Rust's error handling, I wrote a post
that goes through common use cases such as:

* Ignoring the error

* Terminating the program

* Using a fallback value

* Bubble up the error

* Bubble up multiple errors

* Match boxed errors

* Libraries vs Applications

* Create custom errors

* Bubble up custom errors

* Match custom errors

[http://www.sheshbabu.com/posts/rust-error-
handling/](http://www.sheshbabu.com/posts/rust-error-handling/)

~~~
amelius
I'm curious, what did Rust developers think was wrong with exceptions? I'm
pretty happy with them in other languages, as they allow me to focus more on
the "normal" path in my code.

~~~
steveklabnik
The real core of it is that exceptions demand runtime support, and as a
systems language, Rust runs in places where that may be prohibitive.

Beyond that, there are other reasons, but in my mind, that is the fundamental
issue. Everything else kind of rests on top of that.

~~~
amelius
Good point, but C++ exceptions are expensive because they allow you to jump
back multiple stack frames at once. If in Rust you're going to check every
return value, then isn't that _more_ expensive on average?

~~~
Ericson2314
There are more branches, but the branch predictor should be able to figure it
out.

Someday we should push multiple return points, one for each enum variant of
the return type, and then we avoid the branches too. (This approach is
described in [https://www.ccs.neu.edu/home/shivers/papers/mrlc-
jfp.pdf](https://www.ccs.neu.edu/home/shivers/papers/mrlc-jfp.pdf) ).

~~~
Rusky
That would be neat, but it's not a pure win. It's more just a different set of
trade-offs, because it defeats the CPU's return address stack predictor.

For exception-like Result enums where you want to predict the Ok variant every
time that's probably fine (as long as your CPU is new enough that taking the
error path won't cause it to mispredict _every_ return afterwards), but it may
or may not be a win in the general case of enum returns.

------
rklaehn
With the anyhow crate and ? syntax I find current rust error handling a
pleasure to work with.

Of course there are a few rough edges, but nothing major. I also don't mind
having to wrap the successful result of a fn in Ok(...).

I hope "do a few minor things, add better documentation, but leave the
language itself as it is" is also one of the possible outcomes...

~~~
captainmuon
I haven't done much in Rust yet, but it seems you can either use `Result` to
handle errors (and force the caller to deal with it) or crash with `panic`. I
wonder, is there any way to specify "Crash, unless someone up the call stack
deals with this?". What I usually do in other languages is use exceptions as
assertions, when it is just a simple one-off app, but then go back and catch
the exceptions when possible to make it more user friendly and robust. It
seems that exceptions have been omitted on purpose, but is there another way
to express this idiom in Rust?

~~~
imron
> but is there another way to express this idiom in Rust?

If you don’t want to handle Results, just ? all the way up and out of main.
Then come back and deal with them as necessary when you’re ready.

It’s far nicer than dealing with exceptions.

~~~
masklinn
Also `anyhow` exists specifically to support that use-case: you're developing
an application and you largely just want to bubble up errors and show them to
the user, with the odd spot of internal handling.

------
vbezhenar
Despite all modern trends to use return value rather than exception I still
think that exceptions are vastly superior approach to error handling. For me
the ideal implementation is Java without checked exceptions. Rust's panics are
similar to exceptions, so for me the ideal implementation should include some
short syntax to allow converting from Result to result value and throw panic
for errors, but also allow hierarchy of panics and try-catch handling of those
panics (so it would throw some specific panic, not just common panic).

~~~
cies
I'm curious as to why you prefer (unchecked) exceptions over "all modern
trends to use [the] return value" with sum types like Result (Either in
Haskell).

Personally I follow with the trend and feel strongly that the Result-type
approach is superior over (unchecked) exceptions in most cases, since: (1)
they are much easier to reason about for me as a programmer reading and
writing code, as it (2) produces code with easier execution flow and (3) uses
the type system to specifically+exhaustively communicate the possible
outcomes.

I say "in most cases", as I believe there are (probably) some behaviours that
are easier to express using exceptions, but in practice I rarely run into such
cases. And when I run into them, I might simply not be able to come up with
the right abstraction using Result-types.

I find the Rust/Kotlin trend of only allowing one exception (the panic), as to
dictate the language users to use other means to communicate/express multiple
outcomes of a call. Both languages --- by having sum types + pattern matching
switch statements + exhaustivity checks on switches over sum types --- also
provide means to express/handle multiple outcomes of a call, thus rendering
the prevalent use of exceptions mostly irrelevant.

The hierarchy of exceptions that you mention are to me something that goes
against the nature of typing discipline: they subvert the typing system (in
case of unchecked) or add a additional typing component (in case of checked)
to express/handle multiple outcomes of a call.

~~~
jcelerier
> they are much easier to reason about for me as a programmer reading and
> writing code,

I would hate to have to reason about the 250 possible errors coming from every
single library underneath my app, especially when the immense majority of
those are better left to be solved by the user (e.g. file cannot be opened
because the usb stick was removed, audio device unavailable because you are
using a pro audio driver that locks to a single app, the GPU cannot be opened
because I did a driver update and must reboot, etc etc).

Monadic error handling is fine when you have a finite, predetermined set of
"things that can go wrong" \- e.g. you're writing a small input parser for a
text field, either it parses or it does not. But imho this is a microscopic
amount of cases compared to all the "random stuff that can go wrong" in the
average GUI app. And as a GUI app user, I _really really_ hate when the app
tries to do what's best for me - autoreconnect, write to another file, change
the settings automatically, whatever. Just gimme an error popup and let me
decide.

> produces code with easier execution flow

In C++ using RAII objects for state makes that concern pretty much disappear
imho. Save your state & rollback in a tiny type's ctor/dtor and you're good to
go, whatever you do, things can throw and what you were doing will be rolled
back. Not as good as hardware transactional memory, sure, but apparently not a
lot of CPU engineers are able to make that one work yet...

> (3) uses the type system to specifically+exhaustively communicate the
> possible outcomes.

back to 1, this is what I really do not want. Just write the happy / known-to-
the-programmer path and let what you don't know about bubble up to main() or
the top of your event loop.

~~~
littlestymaar
> I would hate to have to reason about the 250 possible errors coming from
> every single library underneath my app

That's is why I think exception are bad: the programmer usually ends up not
thinking about the errors that can occur (and they often cannot even do it if
they wanted because exceptions can come from anywhere) and just propagates to
the user, who often doesn't even have access to the documentation of the
underlying library, and must perform Google search on some cryptic error
message, just to discover that the program is failing because some log
formatting library he doesn't care about cannot have access to the
configuration file it expected. This is a terrible user eperience.

And yes, checking 250 different kinds of error can be tedious (but it should
never be so high anyway), but at least with sum types it's doable because the
type system is here to help you cover every case. And then you can manually
check which error is a reasonable one to return to your user and which is a
recoverable one that should need no external intervention.

If the USB drive has been pulled out too quickly, just tell it to the user
instead of returning a “aborted: did not have the rights to write to the file”
exception.

~~~
jcelerier
> and just propagates to the user, who often doesn't even have access to the
> documentation of the underlying library, and must perform Google search on
> some cryptic error message, just to discover that the program is failing
> because some log formatting library he doesn't care about cannot have access
> to the configuration file it expected. This is a terrible user eperience.

you know what is even worse ? the programmer "handling" the error and just
displaying a "we're sorry, an error has occured" message to the user who now
has exactly zero way to know what is going wrong. Or as seems to be common in
Rust, Go, and other exception-free systems, just calling panic! ... yay,
"error handled".

It is very irrealistic and presumptuous to assume that your program has enough
domain knowledge about its users's systems to handle every single error.

~~~
littlestymaar
> you know what is even worse ? the programmer "handling" the error and just
> displaying a "we're sorry, an error has occured" message to the user

This occurs a lot with language with exceptions too: you need to “handle” the
exception before it reaches the root, otherwise it crashes the whole software
(which is basically the same behavior than a panic in Rust or Go), so people
usually add this kind of catch-all exception handling in Java or any language
with exceptions. Have you ever encountered a “500 internal server error” while
browsing so website? That's it.

The main difference is that you can't do any better when using exception
because you can't easily list all possible exceptions that can reach the root,
while you can in languages with sum type.

Of course there will be lazy programmers in any language, the problem with
exception is that even if you aren't lazy, you cannot really do better than
people who are (unless you can disable exceptions, like in C++ …).

------
thesz
I used to say that what is a library in Haskell is a language feature in most
other languages. Now I see what is library in Haskell is a "Project Group" in
Rust. Rust is much further than most other languages here.

Joking aside, why did Rust not adopted type constructors for a start?

Having "Error Exception Int" would not let you ignore the error when you try
to get that Int. Having "TChan x -> STM x" would not let you perform channel
reading outside of transaction with that channel. Monadic binding will not let
you mix Error and STM computations easily while preserving sequentiality of
code.

Even Kotlin got that, for all PL theory sake. The language that is very close
to Java. I cannot see why Rust can't.

PS And things from this comment [1] would be very much explainable through
"follow the types".

[1]
[https://news.ycombinator.com/item?id=24526604](https://news.ycombinator.com/item?id=24526604)

~~~
steveklabnik
> Now I see what is library in Haskell is a "Project Group" in Rust.

Sort of, a "project group" is a means for organizing people to help accomplish
tasks in the language. (or other parts of the project in general, this group
is under the libs team, which maintains the standard library)

> Joking aside, why did Rust not adopted type constructors for a start?

It is not trivial to adopt advanced type system features in a language like
Rust, and we _have_ been working on getting to this point. It just takes a lot
of time. Associated type constructors are called "generic associated types" in
Rust, and are desired, and being worked on, it takes non-trivial effort.
Higher kinded types are less clear. Monadic binding even less clear than that.

~~~
thesz
Have you tried to express what Rust has with Haskell type system? E.g., to
embed Rust into Haskell?

There were some attempts to do so, for example, [1] and associated code at
[2].

[1] [http://web.engr.oregonstate.edu/~walkiner/student-
theses/mcg...](http://web.engr.oregonstate.edu/~walkiner/student-
theses/mcgirr-18-ms-project.pdf)

[2] [https://github.com/lambda-land/OwnershipMonad](https://github.com/lambda-
land/OwnershipMonad)

I actually am slightly sad that Mozilla started Rust effort instead of
embedding what they wanted/needed to do into some proof assistant or Haskell.

~~~
Ericson2314
Considering the state of "linear Haskell", and the difficulties in working on
GHC (which didn't have CI at the time), I don't really blame them.

I would actually like GHC be a Rust compiler too. I think that would force it
to be more modular, and if it's more modular and has more incremental CI,
we'll get a lot of productivity and such a thing would actually be practical.
So just gotta find a way to unwind the causality loop they (probably
modularity for modularity's sake.)

~~~
thesz
I keep saying that one can embed his language of choice into Haskell and get
whatever one needs. Including, but not limited to, LLVM and other backends.

Linear Haskell is a disgrace, glad you asked. It can be implemented as a
library, as usual [1], and the process of inputting that linear logic into
compiler I see as political, not technical.

[1]
[https://jpaykin.github.io/papers/pz_linearity_monad_2017.pdf](https://jpaykin.github.io/papers/pz_linearity_monad_2017.pdf)

Compiler (ghc) can recognize what code that monad from [1] produces and apply
transformations accordingly. The bonus here? That recognition process can
accidentally optimize parts of program that do not use linear monad at all but
happen to have same structure and same satisfied constraints.

About fifteen-twenty years ago I tried to invent a linear programming language
or add that to Haskell. And it was not pretty. Linearity pops where you
clearly don't want it to be, preventing clear explanation of ideas and/or
requiring several types for innocent expression (there's no possibility of
most general type in some variants of linear logic).

------
ziml77
I'm very happy to see this. I came back to Rust for a personal project after a
long break from using it. The first issue I looked into was the error handling
situation. I found a blog post that covered it pretty nicely and led me to the
anyhow crate, but I would love for there to be official guidelines and for the
features from anyhow and thiserror to be built in.

------
tmpfs
Rust's error handling really made me fall for the language but there are a
couple of pain points for me. As others have pointed out the inability to use
the `?` operator when a function does not return `Result`, it looks like
solutions are in the works for this. But the one i have not been able to
figure out is handling errors neatly at async boundaries, ie, when calling
sync code that returns `Result` inside a `TryFuture` - is there a pattern for
this?

------
cies
I applaud Rusts openness wrt the collaboration by which decisions are made.

------
mjw1007
I see one of the two shepherds for the group is the author of
[https://github.com/yaahc/eyre](https://github.com/yaahc/eyre), which is a
fork of `anyhow` that adds features.

So I expect that's an indication of what's likely to come out of this.

------
tikhonj
I love the flow that the project laid out: survey what the community does ⇒
figure out pain points ⇒ suggest solutions and consolidate. This ensures
suggestions are informed by what already works (or doesn't) and the process
has useful outcomes at each step. Even if the project stalls after its initial
work, it will have documented the error-handling patterns people use in the
wild—legitimately useful by itself.

------
codeflo
I hope that std::prelude::Result becomes anyhow::Result, including the default
generic argument (i.e. Result<u32> just works, no need to write Result<u32,
UnnecessarilySpecificErrorType> unless you have a reason to). That would
encourage examples and simple scripts to use Results and ? instead of calling
unwrap and panicking. Not only would this teach better practices earlier, it
would also address many of the complaints that Rust’s error handling is harder
than exceptions in common usecases.

~~~
danieldk
_I hope that std::prelude::Result becomes anyhow::Result, including the
default generic argument (i.e. Result <u32> just works_

But that's basically giving up on informative library-specific error types. It
may a valid solution, but it is quite a big leap.

Another tack may be to have the functionality of _thiserror_ in the standard
library, to make it less of a hassle to define errors.

~~~
masklinn
> But that's basically giving up on informative library-specific error types.
> It may a valid solution, but it is quite a big leap.

Technically the stdlib could probably define

    
    
        Result<T, E=Box<dyn Error>>
    

That way there wouldn't strictly be any loss, you'd just get a more convenient
"application-level" Result OOTB.

I don't know if that would be incompatible with the existing though.

~~~
steveklabnik
One serious issue there is that this would no longer be in libcore.

~~~
masklinn
Couldn't libcore export the current version and std re-export the aliased
version with a default?

The prelude lives in and uses std, and I don't see people who want exceptions
using no_std anyway.

~~~
steveklabnik
I was thinking no, but I guess you could do something like [https://play.rust-
lang.org/?version=stable&mode=debug&editio...](https://play.rust-
lang.org/?version=stable&mode=debug&edition=2018&gist=7c19e5e077d082eaf56061675cc4d887)

~~~
masklinn
You'd probably need to use the "default type parameter" feature for the alias
otherwise std's Result is a breaking change and you have to use core's result
to provide a custom error type, which I think would be a step back.

It's what anyhow does:

    
    
        pub type Result<T, E = Error> = core::result::Result<T, E>;
    

That way you can still use `Result<i32, MyCustomError>`.

I don't know if there are limitations or issues with this approach though.

------
EugeneOZ
My favorite part of Rust. Please just leave it as is ;)

~~~
octoberfranklin
... only after they ship try_blocks!

[https://doc.rust-lang.org/nightly/unstable-book/language-
fea...](https://doc.rust-lang.org/nightly/unstable-book/language-features/try-
blocks.html)

Seriously, otherwise you can't do error handling within the body of a function
that doesn't return Result<>. The language shouldn't force refactoring
decisions on the programmer like that.

~~~
zozbot234
> Seriously, otherwise you can't do error handling within the body of a
> function that doesn't return Result<>.

Sure you can, just use an inner closure that does return Result<>, and use
`match` or the like for error handling. It's not any more verbose than this
`try {} ... catch {}` special syntax.

~~~
octoberfranklin
> It's not any more verbose

Seriously? I think this is way less readable:

    
    
      (|| -> Result<_,_> { bar(foo()?) })()
    

than this:

    
    
      try { bar(foo()?)? }
    

Also try_blocks don't have to have a "catch". It's just five characters: "t"
"r" "y" "{" "}". I don't see a need for "catch" either.

~~~
Arnavion
The closure version would actually be

    
    
      (|| -> Result<_,_> { Ok(bar(foo()?)?) })()
    

to be equivalent, since bar's Err type might need a From::from conversion via
`?`. So the try-blocks version is even better in comparison.

That said, if you include the `-> Result<_,_>` annotation in the closure case,
you also need to include the `: Result<_,_>` annotation for the binding that
the try-expr is being assigned to, otherwise the compiler won't know which
impl of `std::ops::Try` to use. Of course, you can also use an annotation on
the binding in the closure case and get rid of the annotation entirely. That
is, the comparison is of:

    
    
        let r: Result<_, _> = (|| Ok(bar(foo()?)?))();
    

vs

    
    
        let r: Result<_, _> = try { bar(foo()?)? };

~~~
octoberfranklin
I still find the second case far more readable.

When you see "try{}" it says "this is here because I want to turn on
?-handling within this block".

When you see a closure formed and then immediately applied it says... well I
dunno. Frankly it seems to say "this expression should be simplifiable". In
fact it's a huge wart on Rust that beta-reduction doesn't preserve well-
formedness of expressions.

Also if there were more stuff in the expression body it might not even be
obvious at a glance that the closure is being formed-and-then-immediately-
invoked, leading a reviewer to wonder "wtf is a closure doing here?".

~~~
Arnavion
Oh yes, my point was not that the try-blocks version was less readable. I
agree that it's better and used to use it back when it was still called `do
catch { }` and didn't do automatic-Ok-wrapping. I was just making the
comparison fairer.

------
AlexanderDhoore
A bit of topic but: I write a lot of C and lately I've been using 'bool' as my
error handling type. This probably sounds stupid to many people, but I'm a
minimalist. If a function has an error -> return false.

In the past I would have returned an enum or just int with values for every
error that could occur. But I realized that I almost never actually handle the
error cases differently. I just want to know whether an error did or did not
happen.

This style gives me 99% of what I need, and it's so god damn simple.

~~~
izacus
That works until you find out that you just wrote one of those godawful pieces
of software that say "An error has occured" for everything that goes wrong,
including wrong password, network connection issues or simple
misconfiguration.

~~~
tluyben2
Yep, I was integrating a banking API yesterday and every single error just
shows ‘Something went wrong, contact support.’. Nothing more; I asked if this
was for security (to log and hide the real error as it might give something
away), and their answer was ‘kind of’ as they do not have sane errors, just
stacktraces logged to Sentry. Incredibly hard to debug (as client we get no
access to their Sentry) as you do not know if the authorisation was wrong or
some amount was wrong or whatever. And all errors including not found(404) are
500...

------
jeffrallen
This is great, but I can't help remembering learning Go 10 years ago, when
this was already decided (for better or worse) by the language designers.
Languages are for getting stuff done NOW, not discussing getting stuff done
better, maybe, someday.

~~~
masklinn
You… do realise that you can get stuff done with rust NOW, and this is simply
a project to make things better, right?

Like, I don't know, discussions around generics in some future Go?

Hell, 4 out of the 5 goals stated in this announcement aren't even about
changing anything but rather about mapping out the existing landscape and
clarifying the current best practices, which by definition are already in use.

~~~
jeffrallen
I am also not in favor of Go generics.

~~~
masklinn
But again this has nothing to do with the article, or the thread. Discussing
possible evolutions doesn't preclude "getting stuff done NOW".

