
Things I Enjoy in Rust: Error Handling - ingve
https://blog.jonstodle.com/things-i-enjoy-in-rust-error-handling/
======
zmmmmm
It's interesting to see modern languages tackling checked error handling.
Particularly in the light that Java's checked exceptions are widely considered
to have been a failure. Yet I wonder if Java had better support for things
like union types etc. if it would have turned out differently. After all, it
did offer at least potential to achieve what everyone is after: you can write
a piece of code and know for sure that you handled every possible type of
error.

The most painful aspect of Java's (original) exceptions was how the only
mechanism to handle multiple errors as a single case was using inheritance in
the exception hierarchy. The result was that they propagated through
interfaces and broke downstream code and it became pretty much impossible to
design and maintain a stable API without wrapping exceptions excessively which
in turn destroys the utility of the exception hierarchy, making the whole
exercise futile.

The article seems a little superficial though. Immediately exiting with
'panic' does not seem like a real error handling approach for any realistic
application. It doesn't show how you could handle different types of errors
(eg: file not found vs I/O error reading file) with different cases. The case
it does present is easily handled elegantly by nearly any error handling
design.

~~~
int_19h
I think the biggest problem with Java exception specs is that they're not
composable. In other words, I can't define a method X that calls another
method Y - potentially via dynamic dispatch - and has an exception
specification of "whatever Y throws, plus E".

This manifests almost immediately. For example, suppose you wanted to
implement a key/value database with disk storage. The logical core interface
for it would be java.util.Map, except for the fact that its methods are
declared as non-throwing; thus, you cannot propagate IOException from the
underlying storage layer, unless you wrap it into unchecked RuntimeException.

Conversely, java.lang.Appendable is a very generic interface for appending
characters or sequences of characters to something. But because its designers
knew that _some_ of its implementations would be backed by I/O, all mutating
methods in that interface are declared as throwing IOException. Thus, any
generic code that tries to work with Appendable has to always assue that it
can throw, and has to declare itself as throwing the same exception to
propagate. And now the user of that generic code passes a no-throw
implementation of Appendable to it, but still has to "handle" IOException that
they know will never actually happen.

Or come up with some convoluted scheme, like java.util.Formatter does. This
one wraps Appendable, but swallows all I/O exceptions to avoid having to
propagate them in the most common case, where the wrapped Appendable is a
StringBuilder. But since there are other possibilities, it still has to expose
some API to examine such exceptions, which it does by providing a method on
Formatter that returns the last swallowed exception that was thrown by
Appendable. Of course, now you have to remember to actually check it, and you
will silently get an incorrect result if you do not, which is worse than
unchecked exceptions in the same scenario.

This all gets especially bad one you start working with HOFs, because their
contracts are largely defined in terms of the behavior of functions that are
passed to them. The old Java lambda proposal by Neal Gafter noted this, and
proposed extending the syntax for generics to cover exception specifications
(via union typing, indeed). In the end, they added UncheckedIOException to the
standard library, specifically so that stuff like Stream.map() can be
implemented on top of files, and propagate exceptions, without declaring them.

~~~
zmmmmm
That is a very nice summary!

Some of these tensions seem unresolvable though. At some level if I want to
toss a Map around everywhere and be able to transparently back it with a
database, I'm going to have to accept that operations might fail. I can't have
it both ways. In the end, the abstraction is "leaky". I don't really see how
Rust or anything else can make that go away.

I feel like the closest I can get to have my cake and eat it too is by having
the compiler transparently infer all the exceptions / errors that can be
thrown by the calls I am making (like type inference for variables), but leave
them out of the type signatures. Then I can opt in to enforcing the checking
of them when I want to, so something like

    
    
        check {
           doSomething()
        }
        catch(...) { // every error now has to be handled
            ....
        }
    

I guess this is something like what Rust is achieving through the ? operator.

~~~
int_19h
In the case of Map, since some maps fail and others don't, and those that fail
can fail with different errors, it would be reasonable to express in in the
type itself - i.e. instead of Map<T>, you have Map<T, E> (and then you also
need union types to properly express E being a list of exceptions, and a unit
type to express an empty list). Then any code that handles generic maps can
also be generic wrt E, propagating whatever is thrown, and possibly adding its
own errors, or catching some specific ones. And, conversely, you could only
accept Map<T, Unit> for code that expects non-throwing maps, and then you
don't need to catch or propagate anything in that code. Add defaulted generic
parameters for convenience - e.g. Map<T, E=Unit> \- and now API clients don't
have to care about E if they don't want to, and are happy with things as they
are.

Alternatively, you can just say that map is something that never throws, by
definition. But, of course, that reduces the utility of the interface, by
making it less generic than it could be. In some cases - especially with HOFs
- the reduction in utility can be so substantial that it negates any gains
from code reuse.

------
uzername
I've shown Rust's Result type to folks on occasion because I like it so much.
The other aspect I like is the `?` operator, which allows for easier error
propagation.

[https://doc.rust-lang.org/book/ch09-02-recoverable-errors-
wi...](https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-
result.html#a-shortcut-for-propagating-errors-the--operator)

~~~
fnord77
`?` isn't really that great - for all but toy cases you have to jump through
hoops - especially if you are trying to handle different error types from
separate in a unified way.

Not a fan of Result, either - it ties together the error type and the type of
the desired item - these should be separate concerns.

~~~
0815test
> Not a fan of Result, either - it ties together the error type and the type
> of the desired item - these should be separate concerns.

How so? 'Result' is just a generic variant record with two variant cases, for
the "success" and "error" condition. It's the exact same as the `value, err`
pattern in Go, except that it additionally enforces proper semantics and
handling of the outcome.

------
wyldfire
One of the more interesting semi-recent additions to stable rust is the
question mark operator. It's not covered in this Rust Error Handling article,
but it's got some of the early-exit appeal of exceptions without too much
unwind magic.

Unfortunately I haven't been able to get the question mark operator to work
yet with my code, but it looks appealing. It's not 100% obvious to me what
qualities my Result type needs in order to be able to make it work. Or whether
it's bound in practice to the Result type(s) defined in std. I'm sure those
details are all in the docs somewhere but I spent the time to look yet.

~~~
CUViper
One thing you may be missing is that the `Result::Err` in your return type
must implement `From` for the `Result::Err` type where you're using the `?`
operator. That's already covered if those `Err` types are identical (`From<T>
for T`), otherwise you need to implement the conversion.

    
    
        impl From<QuestionError> for ReturnError {
            fn from(error: QuestionError) -> ReturnError {
                // TODO
                unimplemented!()
            }
        }

~~~
wyldfire
Oh yeah, that's likely it. Thanks!

It it safe to just put a noop in there? What's From-ness? Is QuestionError
defined in std? Where do I read more?

EDIT: oh I see, I didn't read carefully. I just need to provide a conversion.
Your example is very clear.

~~~
ds206
It's how you explicitly cast from one type to another. To use the ? operator
with a custom error type, it needs to be converted to the std error type.

~~~
steveklabnik
It needs to be converted to whatever error type the function returns. That can
be anything.

------
dep_b
I'm kind of contemplating a career switch since in mobile JavaScript cross-
platform seems to become pretty big and I just don't feel very warmly about
becoming a JavaScript specialist.

What are the possibilities for somebody that's pretty heavy iOS senior to
transfer to a role building applications or services in Rust (or Elixir, for
that matter)?

~~~
rhinoceraptor
AFAICT, 99% of the Rust jobs out there are just crypto startups. So at this
point, not that great.

~~~
agildehaus
Any job is a Rust job if you're in charge of deciding the language.

~~~
rhinoceraptor
It's a tough sell to manglement considering there's almost no developers who
know it, and it's not something you can ramp up on quickly like Java to C# or
Ruby to Python.

~~~
miohtama
I remember that in the past, early 00s, this was true for Python as well.

One had to make an active effort to pick Python to be a tool for the job.

I remember being in one of big government project pitch session and the
project manager asked "What this Python language? Sounds like some toy."

Times will change. However Rust sweet spot is system programming. Even though
one could build e.g. websites with it, Rust loses its edge and becomes
verbose, a too heavy hammer.

------
icxa
I am a full time Go developer learning rust on the side for more low level
things, and one of my side projects is writing an OS.

Rust's error handling really feels the exact same way to Go's in my opinion,
the only difference is, so far in my admittedly newcomer viewpoint, using
unwrap to get to the error value, or returning an error value and doing an if
err != nil check.

This isn't to say anything other than agreement that I indeed like this style.
I like to handle my errors as values and program around them.

The thing I don't get is why all the bikeshedding and hate around "if err !=
nil" spam I seem to have seen here often and on other blogs. Go seems to get
this "criticism" (I don't really believe it is a valid critique, for what it
is worth) heaped onto it plentifully, while Rust seems generally immune to it,
otherwise praised for the approach -- but it is the same approach! In fact you
even have a builtin panic func in go!

Can someone tell me what I may be missing?

~~~
andrewjf
The best parts of rust's error handling are the try!() macro (aka `?`), as
well as From/Into error types allow the compiler to wrap types or convert
between error types. This eliminates almost all of the go boilerplate `if err
!= nil` and you only handle the error where you actually need to with pattern
matching and destructuring, instead. It is much nicer. Go's error handling is
pretty primitive, in my opinion.

~~~
aksx
>The best parts of rust's error handling are the try!() macro (aka `?`),

Go's way of providing try!() macro is less magical but almost as useful.[1]

> From/Into error types allow the compiler to wrap types or convert between
> error types

error is an interface in Go which can be easily cast/checked for the
underlying type.

> Go's error handling is pretty primitive

I wouldn't call it primitive, I would call it simple. I like the comparison i
read on a blog on HN.

Rust is the 'new' C++ and Go is the new C

[1] [https://blog.golang.org/errors-are-
values](https://blog.golang.org/errors-are-values)

~~~
xfer
> error is an interface in Go which can be easily cast/checked for the
> underlying type.

Yeah it's done during runtime and the compiler won't be able to help you with
it if you fail to do exhaustive type checking. It's a problem anytime you
refactor your code. ADTs and pattern matching is pretty much the bare minimum
language feature i expect from any statically typed language.

------
csomar
I don't particularly like the solution presented.

1\. The Rust community is biased against the usage of "unwrap". It was
supposed to make prototyping easier. It should not have been there and should
not be used.

The correct way will be to "evaluate" the function and either return a result
or "propagate" the error to some part of your application that will handle
"all" the errors. Whether they are your code errors, or errors thrown by other
libraries.

2\. The code will look even more succinct with the "?" operator:

> let value = some_operation_that_might_fail()?;

3\. If the function fail, the program should handle the failure. By design, a
failure should not result on setting a "default value". That's not an error
but more like an "option".

The correct type the guy should have used is the Option type. Or more like an
option inside a result. The option could return a value or not. If not, you
set a default value.

~~~
epage
> 1\. The Rust community is biased against the usage of "unwrap". It was
> supposed to make prototyping easier. It should not have been there and
> should not be used.

I'm a little confused on this point you are making. You say the Rust community
is biased against `unwrap` which makes it sound like you think that is wrong
but then you proceed to say it shouldn't be there, making it sound like you
agree.

`unwrap` has legitimate uses in product code as an assertion. Normally I
recommend `expect` instead so that the assertion is self-documenting but there
are times where the the scope of the assumption you are making is so small, it
is obvious and doesn't need a comment, for example if I partition a collection
based on the result [0]

[0]: [https://doc.rust-lang.org/stable/rust-by-
example/error/iter_...](https://doc.rust-lang.org/stable/rust-by-
example/error/iter_result.html#collect-all-valid-values-and-failures-with-
partition)

> . If the function fail, the program should handle the failure. By design, a
> failure should not result on setting a "default value". That's not an error
> but more like an "option".

From the function's perspective, it errored, but from your perspective, it
failed and you want to fallback to a default value.

Yes, someone could express this instead as an `Option` or a `Result<Option,
_>`

\- Sometimes you just don't need that extra boiler plate \- Sometimes you want
to provide context for why something failed in case the user considers it an
error as well. You can't express that with `Option`.

~~~
csomar
> I'm a little confused on this point you are making. You say the Rust
> community is biased against `unwrap` which makes it sound like you think
> that is wrong but then you proceed to say it shouldn't be there, making it
> sound like you agree.

Nope. I think unwrap shouldn't be used without understanding the error
handling capabilities of Rust. It also shouldn't be used for deployed
software. The problem is that most people introduced to Rust would like to see
something working now and Rust has higher barrier to entry than most
mainstream languages. They'll likely keep the habit.

> From the function's perspective, it errored, but from your perspective, it
> failed and you want to fallback to a default value.

That's not my point. My point is that a failed function should not return a
"default value". A default value is not a failure but rather a design
decision. Should the function fail, it returns an error.

~~~
burntsushi
> It also shouldn't be used for deployed software.

This is equivalent to saying that `&vector[i]` shouldn't be used in deployed
software, since it's just syntactic sugar for `vector.get(i).unwrap()`. It's
also equivalent to saying that `assert!(...)` should never be used in deployed
software. Or `unreachable!()`. Or any number of other things.

unwrap is perfectly fine for use in production. Stating a requirement in terms
of unwrap doesn't make sense outside of niche scenarios. It's too specific and
not really actionable in a lot of cases. A better way to put it is that
programs that surface a panic to end users of the application have a bug. If
the panic is from inside of a library and not the result of documented
preconditions on a function, then the library has a bug. Otherwise, the bug is
in the application.

------
bfrydl
While I too love Rust's error handling, this explanation of it seems
incomplete without discussing the ? operator and its interaction with the From
trait. The convenience methods on Result itself are nice of course but they
aren't really the main mechanism of error handling in my opinion.

------
gambler
So what do you do when you want centralized error handling, which is the whole
point of try-catch idiom?

 _> he second side effect is that it forces you to think about how to handle
possible errors._

Java does this with throws E and (at least in Java) it sucks and I very much
prefer C#'s implicit throws. In fact, I don't think it goes far enough. If you
have complex error recovery, the language should allow you to completely
separate good executions paths from the bad paths. Scope guards are a step in
the right direction.

How would Rust code looks like if the error handling logic was something like
"try X, if it fails try again, if that fails schedule another attempt via a
queue, log a warning; if scheduling fails log a fatal error"?

Sure, handling this stuff with try-catch looks horrible. But I've thought long
an hard about how to simplify it and using tricks like .unwrap_or (which you
can simulate in C# to some extent) does not cut it, because error handling
often requires to operate on method-scoped variables (e.g. for logging).

~~~
ChrisSD
Depends what exactly you're looking to do. You can propagate errors with the
`?` operator. And you can match on the specific error at a higher level.

The docs[0] do give a more complex example of error matching:

    
    
        fn main() {
            let f = File::open("hello.txt");
    
            let f = match f {
                Ok(file) => file,
                Err(error) => match error.kind() {
                    ErrorKind::NotFound => match File::create("hello.txt") {
                        Ok(fc) => fc,
                        Err(e) => panic!("Tried to create file but there was a problem: {:?}", e),
                    },
                    other_error => panic!("There was a problem opening the file: {:?}", other_error),
                },
            };
        }
    

Although they also provide a slightly simpler alternative for this particular
case.

[0] [https://doc.rust-lang.org/stable/book/ch09-02-recoverable-
er...](https://doc.rust-lang.org/stable/book/ch09-02-recoverable-errors-with-
result.html)

~~~
gambler
Doesn't look that much cleaner than try/catch, to be honest.

What I would _really_ like to see in C#:

    
    
      public int DoThing(int y) 
      {
        //logic
      } 
      catch (SomeException e)
      {
        //handle things
        return 0;
      }
    

This would remove a lot of useless syntactic garbage without the need for any
radical changes in the language. (It would translate to try-catch that wraps
the entire body of the method.)

But so far I gave up on syntax-level error handling of any complexity.
Generally, I just write multiple "agents" and all the error handling, retries
and recovery is simply done by separate agent(s). The agent that tries to
perform the initial actions simply records that it failed in some persistent
storage and terminates or goes on to do something else.

~~~
ilitirit
> It would translate to try-catch that wraps the entire body of the method

It would not not be the same unless you change how scoping works in these
instances, and that can introduce other complexities, e.g. how to limit scope
so that some things are inaccessible to the catch block.

~~~
gambler
This:

    
    
      public void DoThing(){
         //code
      } catch (Exception x){
      } finally {
      }
    

would simply be syntactic sugar for this:

    
    
      public void DoThing(){
        try {
          //code
        } catch (Exception x){
        } finally {
        }
      }
    

It's consistent.

~~~
ilitirit
Yes, and that only works by changing how scoping works. Unless of course you
don't care about the variables created inside.

i.e.

    
    
        public void DoThing()
        {
           var someVar = SomeStaticClass.FooFunc();
           someVar.DoSomething();
        } 
        catch (Exception x)
        {
           // Scoping rules will need to change
           // to put someVar in scope:
           _logger.Log($"{someVar.GetId()} is invalid");
        } 
        finally 
        {
           someVar.Cleanup();
        }
    

And this also implies that there's no way to make a variable declared inside
the method inaccessible to the catch block without additional changes to the
language.

~~~
gambler
_> Unless of course you don't care about the variables created inside._

In majority of cases I really don't care about the variables created inside.
If I can't handle an error just by looking at the exception and method
arguments I usually refactor. In my view, error handling should not be
intertwined with the "happy path". It makes reasoning about non-trivial code
much harder.

For other cases the old syntax will still be available.

And yes, the value of "finally" in such a shorthand is dubious. I just added
it to make it clear how the rewrite rule would work.

------
BooneJS
TensorFlow has a StatusOr C++ class that allows functions to return a value
that can be first checked to be result.ok() (no error), and then the actual
value can be unwrapped. It’s Google’s way of not using exceptions.

[https://github.com/tensorflow/tensorflow/blob/38c762add3559b...](https://github.com/tensorflow/tensorflow/blob/38c762add3559b3292139c1d839c706a1695e2fc/tensorflow/stream_executor/lib/statusor.h)

------
namelosw
Designs like Java which have NullPointerException simply ignore null. Modeling
T instead of T | null is just too convenient. It's a lazy design.

I'm fine with most of those new ways of error handling, although some feel
better and the others feel a little bit awkward. If some concept exists
(exception/emptiness/async), model it, handle it. Instead of just ignore it
and leak the responsibility to the users.

------
XCSme
Not being familiar with Rust, the article was interesting, but I felt like it
lacked examples. The Result<T, E> is also available in other languages, even
in TypeScript you could have `function x(): number | Error {}`, it is just a
consequence of being able to return a union of data types, why is Rust's case
special? (apart from being able to handle it nicer with "match")

~~~
guntars
Rust’s is a tagged union so you can have Result<number,number>, something you
can’t have in Typescript.

~~~
59nadir
You can have `Result<number, number>` in both.

You generally have to treat `E` as an error because of the semantics of
`.map()` in every language, though, because you have to essentially ignore one
of the sides. `map<Result<T, E>>` is untypable for any practical scenario
where both sides matter equally.

Also, just to clarify how you can have essentially the same thing you would in
f.e. Haskell but in TypeScript:

```

    
    
        export type Result<T, E> = Ok<T, E> | Err<T, E>;
    
        interface Ok<T, E> {
          type: "Ok";
          unwrap(): T;
          map<B>(f: (x: T) => B): Result<B, E>;
          toMaybe(): Just<T>;
          or(errorValue: T): T;
          apply<U>(f: Result<(x: T) => U, E>): Result<U, E>;
          bind<U>(f: (x: T) => Result<U, E>): Result<U, E>;
        }
    
        interface Err<T, E> {
          type: "Err";
          unwrap(): E;
          map<B>(f: (x: T) => B): Result<B, E>;
          toMaybe(): Nothing<T>;
          or(errorValue: T): T;
          apply<U>(f: Result<(x: T) => U, E>): Result<U, E>;
          bind<U>(f: (x: T) => Result<U, E>): Result<U, E>;
        }

```

~~~
excepttheweasel
Right, but in Typescript you can't match on the type at compile time, you have
to add a "type" field with a string tag and match on it at runtime.

~~~
59nadir
Well, the compiler constrains the type automatically as far as it can go, so a
`switch` on the `.type` property will constrain it down to the specific case
in the union. Practically speaking it's not all that different to f.e.
Haskell, except you're manually tagging the different cases. You get
exhaustiveness checks when checking cases by adding a `assertExhaustive(value:
never)` in your `default:` case, which will only match if you handled all the
cases.

For development purposes this is all essentially the same as proper sum types,
_with worse ergonomics_.

My point was mostly that practically speaking you can have a full featured
`Result<T, E>` type that is certainly good enough. What TypeScript lacks is
what most halfway languages lack; higher-kinded types, etc., and good
ergonomics that follow, such as type classes. These are also not a given in
more esteemed type system circles: OCaml doesn't have them either (and most
features that are "coming soon" in OCaml are bordering on vaporware).

Most nice type system features you can get by way of combinations of 2-3
distinct type system features in TypeScript. In terms of end results the only
things that differentiate TypeScript from languages with more traditional type
systems is that you have to use `TSLint` to absolutely eradicate `any` from
usage completely (wheras you don't have it at all in a better type system) and
the ergonomics for functional programming just aren't that great in JS.

This isn't the TS part of the syntax that's the issue, it's just that anyone
who's used a ML descendant in the last 40+ years will have noticed that
(automatic) currying is a massive boon to functional programming (and on top
of that C-syntax is just about the worst for FP with all the noise that comes
with just calling functions). Even something small as not being able to define
operators is pretty disruptive to nice code. People like to rag on languages
that let you do this, but there are many patterns in FP that absolutely are
best encoded via well known and generally accepted operators.

------
gamma3
Really nice! The Result reminds me of Validation from Scala.

In Scala you return a Validation that is either a value or an error, and can
pattern match on it. Also: trySomething().orElse("Default value")

------
CuriousSkeptic
To be fair. The idiomatic C# would be:

    
    
        if(TrySomeOperationThatMightFail(out var value))
        {
        }
    
    

But even with that I often wish it was more like the Rust example

~~~
snypox
Except 99% of methods are async in a web app where the out variable does not
work.

------
CountHackulus
This is an extremely common pattern in Erlang and is super useful so it's nice
to see Rust embrace it.

------
miohtama
The article is a good opinion, but lacks necessary wider discussion in the
context of all programming.

1) Is Rust error handling somehow better than in a programming language X?

2) Is it because of Rust or because standard libraries or something else
(developer laziness, writing silent exception eating)

3) Why other programming languages choose to do it other ways?

Now the article reads like Rust education, which is good, but other commenters
here find it very subjective.

