Hacker News new | ask | show | jobs
by nicoburns 2607 days ago
> Java, C++ and Python have exceptions to keep you from littering your code with error handling, and code with exceptions is hard to read. The line of code you are looking at any point in time may instantly jump to another place, effectively,

Rust and Swift both manage to make error handling easy while keeping this nice "errors are just values" property. Go could easily have added Rust-like or Swift-like error handling backwards compatibly with their existing idioms without falling back on weird special cases of format strings, but have chosen not too.

This does not make the language simple (even though in other ways it is).

1 comments

How is rust error handling better than gos?
Okay, I'll bite.

It's better in many ways.

Firstly, rust has sum types, so it's possible to have exhaustive matches and know you've handled every case. This isn't possible in go. For comparison:

    // go
    val, err := doSomething()
    switch err.(type) {
    case *SomeErrorType:
      // handle
    default:
      panic("inexhaustive error check (at runtime, only if that error type is hit)")
    }
    
    // rust
    let (val, err) = do_something()
    match err {
      Err::SomeErrType(inner) => { /* handle */ },
    }
    // won't compile if the match isn't exhaustive
Note as well that you have to return the error interface, not some more specific type, in go because of the "nil struct is not a nil interface" gotcha. Juggling around structs that are returned as errors is basically impossible to do safely, so everyone returns the error interface. This is another way the language causes error handling to be worse.

Next, generics in rust allow for nicer chaining of computation in the presence of errors. Let me show you two examples. Again, the go and rust code is as identical as I can make it:

    // go
    val1, err := computation1()
    if err != nil {
      return nil, err
    }
    val2, err := computation2(val1)
    if err != nil {
      return nil, err
    }
    return computation3(val2)

    // equivalent rust
    computation1().and_then(computation2).and_then(computation3)
    
    // also equivalent rust
    let val1 = computation1()?;
    let val2 = computation2(val1)?;
    computation3(val2)
The ability to have a generic 'Result<T, E>' type to chain computation allows for code to be more readable, while still having all the benefits of errors being values.

The ability to have a generic 'Option<T>' instead of 'nil' also is very helpful, but enough has already been written about null pointers that I don't wish to rehash it here.

Finally, in practice, rust's higher level features (namely macros) allows libraries to create very powerful developer abstractions around errors, like those offered in the 'failures' crate, all without having any runtime overhead.

In practice, all of these things also combine to result in libraries offering better error types and allowing callers to handle errors well.