This is not correct. Exceptions do different things than report an error. They unwind the stack. That's why they are called exceptions and not errors.
One important benefit of Go's error handling pattern is readability. With exceptions, it's not easy to see who handles it and where. There is indeed less code, and that's nice for the writer, but from the reader perspective, error handling becomes obscure. And from the quality control point if view, this becomes unsafe.
The parent is correct. Returns also unwind the stack.
> One important benefit of Go's error handling pattern is readability
I beg to differ, Go's approach is similar to checked exceptions, Java's original sin. And just like checked exceptions, forcing the invoker of a function to handle the error directly is the wrong approach in the vast majority of cases. It just produces code noise and catch/wrap/throw style code, commonly found in old Java enterprise projects. This obsfucates the default path and makes middleware very hard to write.
> With exceptions, it's not easy to see who handles it and where.
Making errors part of the function signature encourages developers to handle them directly at the call site. Which is where most buggy and unreliable error handling is found. The default approach of safely unwinding the stack until you reach the http handler (or equivalent), returning 500 applies to error codes as well. It should be simple to do, automatic even, so novice programmers write robust code out of the box. Hence exceptions.
Seriously, exceptions are very different from returning an error. Confusing error return with checked exception tells it all. A checked exception is just an exception type specification.
When you read code with a call to a function returning an error, you see how the error is handled. With exception, unless there is a try/catch close surrounding the call, you don't know where and how an exception is handled.
To me, throwing exceptions is like littering the streets. That feels fine for the one who does it, because he assume someone else will take care of the mess. But the problem is taking care of it, who, how when ? With big projects, this strategy is unmanageable.
Correctly handled exceptions don't make middle-ware easier to write or more readable. On the contrary.
You know that programs are not only http handlers, right ?
> A checked exception is just an exception type specification.
A checked exception _requires_ an exception type specification. If you don't handle the exception locally, that is. It's this quality I refer to, when I say checked exceptions are similar to error codes: an API designer, without knowing the full context, requires the call site to do something about it, even if 9/10 call sites could not care less, _especially_ in big projects. Some other commenter linked to this interview [0], which elaborates on that problem.
> With big projects, this strategy is unmanageable.
Clearly, it's possible to have mantainable projects both with and without exception handling.
> You know that programs are not only http handlers, right ?
I find this somewhat condescending, but yes, I do know that. Most programs have system boundaries though, and might recover from quite severe error conditions there.
> […] function returning an error, you see how the error is handled.
In practice you only see that errors get returned immediately. Most functions rightly give up rather than trying to handle errors because they don't know exactly how or where they're being (re)used.
Every function has an "exceptionExit:" block, by default it does just "return err;"
After every function call, an automatic
"if err != nil {goto exceptionExit}" is added.
You can add an "exception" block to a function, it replaces the default.
Now you have function level exceptions in Go just by syntax sugar, without stack unwinding and without requiring new compiler functionality, just syntax sugar.
That's my point. returning the err until some caller above you handles it is unwinding the stack. You're just forced to do it manually at every single level of the stack.
> One important benefit of Go's error handling pattern is readability.
Maybe; personally I find it increased clutter that obscures readability (much as do checked exceptions.)
> With exceptions, it's not easy to see who handles it and where.
Who handles it and where is the one thing that is explicit and readily apparent with unchecked exceptions. What can be harder to see with unchecked exceptions than with error returns or checked exceptions is who (other than the original source) throws it and requires consideration of handling it or ignoring/rethrowing it in the caller.
At the cost of making the entire logic's readability less which to me is more important than sometimes getting confused where errors bubble up to.
The philosophy is different when, for example the author of Ruby wanted to make coding fun for programmers and does a good job at it and Go is sticking to 'this must be right' approach and breaks some people's heart.
I used ruby for a long time and Go more recently. I think Go is fun. I’m able to read code bases with consistency. In a lot of Ruby apps, I see creative flexing that is unique to that person, or teams style.
The fun part is getting code written, and shipped. And it stays fun when it’s maintainable and production ready.
I’m having a lot of fun shipping Go code. :) I definitely can understand a codebase a lot faster than a random ruby one. That may be a personal thing but it works for me.
Depends on your definition of fun, I guess. I personally don't like Ruby because of that fun-factor. In most cases, it makes programming easier for the novice, but more complicated for the experienced.
This is because to make it easier for the novice, there are all kinds of constructs that try to make the code imitate normal English. But coding software is a completely different thing than writing text, thus the English-like front is in fact a smoke screen that hides the real gears.
A small example would be the unless keyword. It completely throws me of each time I come across it, because it reverses the order of evaluation:
Do something, unless condition applies.
I read that from left to right, so in my mind "Do something" has already executed, but then I have to go back, because the condition might not apply. This get really 'fun' if the condition is something negative.
I like Go just because it way more simple. Even it is a bit more verbose in the error handling, everywhere else it is very minimal and clear.
Error/Either monads are the perfect middle ground IMHO. You get errors as data types and an efficient way to abstract away the boilerplate associated with it.
Yes, I quite liked this about Rust's `Result` and `Option` type and using monads in general but I don't think Golang could achieve this pragmatically without generics.
Claiming "errors are values" is as useless as claiming anything in computing is "a value" (because it is).
Errors are not just values, however, they're something much more specific: An uncaught error is a specific circumstance where the programmer's mental model was insufficient to account for all the state possibilities. Literally the introduction of the unexpected. And this should be treated as a very bad (or at least a very special) thing, as soon as possible. Hence, runtime exceptions. (Hence, disclaimer alert, I'm not a fan of Go.)
One important benefit of Go's error handling pattern is readability. With exceptions, it's not easy to see who handles it and where. There is indeed less code, and that's nice for the writer, but from the reader perspective, error handling becomes obscure. And from the quality control point if view, this becomes unsafe.