Go, however, forces you to spread your error handling over a thousand little pieces with zero overview or control of what's happening.
Rust eventually realised this and introduced try! and ? to simplify this
Go, however, forces you to spread your error handling over a thousand little pieces with zero overview or control of what's happening.
Rust eventually realised this and introduced try! and ? to simplify this
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.It is unfortunate that many of Typescript developers still rely on throwing exceptions around (even in their own typescript code). Result types are totally doable in Typescript and you can always wrap native calls to return result types.
90% of the criticism of exceptions I see comes from the bizarre and mistaken idea that every call needs to be wrapped in a try block and every possible error mentioned.
That was prototyped around Rust 0.4, so I wouldn't say "eventually" :)
Is there in practice a significant difference between try/catch and Go's "if err" ? Both seem to achieve the same purpose, though try/catch can cover a whole bunch of logic rather than a single function. Is that the only difference ?
I think that's the biggest difference.
With Go you need to specifically check the errors and intentionally decide what to do, do you handle it right there or do you bubble it upwards. If you do, what kind of context would the caller want from this piece of code, you can add that too.
Compared to Go, this is actually better because the type system can tell you what kind of errors to expect, instead of just "error".
Is this really all that interesting or worth the LOC spent on error handling when 99.9999% of the time in practice it’s just bubbled up?
And any “context” added is just string wrapping. Approximately nobody types golang errors in a way that lets you programmatically know what went wrong, to be able to fix it in-line.
I think I would be more empathetic to the arguments defending golang here if I’d ever worked or seen a project where people actually handled errors instead of spending 2/3 of their time writing code that just punts on any error.