More importantly, Rust has the notion of a
result type and it is designed to be both generic and composable.
A problem I often face in Go and TypeScript code is code that ignores errors, often unintentionally. For instance, many uses of JSON.parse in TypeScript do not check for the SyntaxError that may be thrown. In Go, it is common to see patterns like
_ := foo.Bar()
// assume Bar() returns error
This pattern exists to tell the reader "I don't care if this method returns an error". It allows one to avoid returning an error, but it also stops the caller from ever being handle to the error.
Also, the position of the error matters. While the convention in the stdlib is to return errors as the final value, this isn't necessarily followed by third party code.
Similarly, errors are just an interface and there is no requirement to actually handle returned errors. Even if one wants to handle errors, it's quite awkward having to use errors.As or errors.Is to look into a (possibly wrapped) chain of errors.
The benefit of Rust's Result<T, E> is that
- position doesn't matter
- there is strong, static type checking
- the language provides operators like ? to effortlessly pass errors up the call stack, and
- the language provides pattern matching, so it's easy to exhaustively handle errors in a Result
The last two points are extremely important. It's what prevents boilerplate like
if err != nil {
return nil, err
}
and it's what allows one to write type-safe code rather than guess whether errors.As() or errors.Is() should be used to handle a returned error.