func test() (result interface{}, err error) {
_, err = someOtherFunction()
}
The thing is, you probably want to handle the error eventually, and that is just as verbose in Rust [1] as it is in Golang (IMHO). One can of course debate if it's more pleasing to use a "match" operator than an if/then/else loop to handle errors, but to me the difference is marginal. BTW one could even mimick Rust's type-based errors in Golang by returning a single interface{} type that's either a result or an error and then using a type switch, but that seems just silly and I have never seen that in the wild.1: https://doc.rust-lang.org/rust-by-example/error/multiple_err...
In go, you constantly have to `if err != nil { return }` even with named return values. This is the part that breaks up the flow constantly and hurts readability.
In Rust, the ? macro also handles this.
Rust's '?' sugar effectively replaces the whole `if err != nil { ... }`, meaning it wasn't really honest to list Rust as another language that proves that error handling just has to be verbose and repetitive.
I used to like passing errors up the stack like you describe, but this invariably leads to hard-to-debug programs that terminate with unspecific errors like "i/o error", leaving it to the user or developer to figure out in which exact branch of the 9-layer call stack that error originated. If your error handling is exception-based and if your interpreter/runtime generates tracebacks you can do that, but not if you explicitly handle errors within the regular control flow.
You might want to add a caveat to 'invariably', something like: 'provided you're using a language where errors are modelled as one big mashed-together string' ;)
If we assume the basics of how Go is designed are an invariant, I think the best change would be adding syntactic sugar to return a struct satisfying the error interface, annotated with the (statically encoded) symbols and line numbers, and then also the arguments for those function calls. I have never seen a Go codebase whose hand-written 'contextual' error handling blocks provide even this, let alone more useful information. If you have, I'd be interested to hear about it!
failed to start a session: failed to read the session config file: i/o error
This is perfectly equivalent to java.lang.IOError
in startSession() line 192
in readConfig() line 123
in openFile() line 131
Except that with Exceptions you get this for free, with more context than naive wrapping (and much more than `if err != nil { return err }` ).Usually with exceptions, you want to catch and add more context at every layer boundary, and get the call stack for free.
Also, raising and catching exceptions is expensive in many languages. I know that in Java it's fast, but other languages like Python produce significant overhead when creating and raising an exception, that's something one should consider as well.
Also, the performance argument counts against your point, not for it. Exceptions rarely happen (when used properly), and when they are not raised, they cost nothing. Whereas go-style errors require explicit checks every single time you execute an operation which can fail.
But I think that pattern matching, and even checked exceptions, are much less verbose than constantly checking the return value. That doesn't mean that they're better, just less verbose. (Exceptions in particular - I can invisibly leave this function at points that are not specified. Does that leave everything in a clean state in all possible circumstances? Explicit error returns are much more verbose, but may be easier to reason about, because the information you need is all explicitly visible.)
I don’t see how you can say that monadic results aren’t strictly better: they’re safer, less verbose, and give the option of abstracting around. Odds are they’re even more efficient.