I'd be interested in hearing more about the exceptions side. The usual derision is that some top level unrelated code ends up handling them, but that seems silly. A far more normal example would be a routine that has to get some resources and calls various retrieval routines which may end up accessing the filesystem, databases, or the network. Those routines could go several calls deep before throwing an exception. The top routine can then find alternatives, use cached versions, return an error etc.
I do like exceptions in Python where their use is ubiquitous and there is garbage collection. I detest checked exceptions in Java because you are forced to handle things at a level you often don't want to.
I'm 65% certain Go made a mistake not using exceptions, but would love to hear from others.
There is an exception-like mechanism called 'panic', but it shouldn't be used as a control flow tool. Panic is reserved for unrecoverable errors, not mundane situations like a failed query.
This seems extraordinarily painful. I usually have five or six stack frames between GetDataFromDatabase() and code that can raise error dialog to user or return HTTP error code. This means every single one of those stack frames are going to duplicate this annoying if err != null sequence.
The article author is quite wrong when he says "exceptions are broken by design because they force errors to be handled at points far away from the cause". Exceptions don't force you to handle errors at any particular location; they allow you to handle errors anywhere in the stack. On the other hand, return values do force you to handle errors then-and-there, which 90% of the time is a half-dozen stack frames away from your handler.
We have two orthogonal tasks which traditional exception mechanisms complect together:
1. Handling a failed assumption, which will cause the code that follows to be incorrect (I need the contents of this file to do my task, but it doesn't exist, so I can't do my task)
2. Handling a failed operation, which there is a straightforward way of working around (if I can't write to my log file, maybe write to stderr instead instead and give up)
The trouble is that the 'inner' code doesn't know which of these a given failure is, because it is context specific. Seems like with languages like Java, the default is 1, whereas with Go it is 2. They're kind of equivalent, though, because you can always convert a !ok into a panic(...).
But certainly exceptions are the nuclear option, so it seems reasonable to me that they shouldn't be the default for common operations that can fail.
Why wouldn't you want to use exceptions for #2? I posit that it is very rare for errors like RPC failures and filesystem errors to be handled at the level of the call. 90% of the time the natural handler is several stack frames up where you can raise an error dialog or write an http error response.
Return values create endless repetitive "if error return error" code, or worse - programmers get lazy and ignore return values, producing bugs that show up only in hard-to-test-for situations like transient network failures.
Why is it rare for RPC/filesystem failures to not be handled at the level of the call?
I think its much more natural for a memcache API to return an error if the server is not reachable, and I can continue to execute the current function. Similarly, I think its more natural for a "users" API to return an error if a particular user doesn't exist so I can redirect to a signup page or something, rather than throw a UserNotExistException.
And yes, errors as return values may seem to add more code to simple examples. But I find it does wonders for clarity/readability. Using "regular" control-flow for error conditions and the "happy path" makes code much easier to follow; this is as opposed to trying to intuit the different ways control can jump from the happy path into the error handling.
Also, I find that having to write the "if error return error" makes me pause to think about how to handle errors properly. For example, if the function I'm writing literally cannot proceed I will return the error. If its a really weird place to be getting an error, write it to a log and return the error. If I can ignore errors (like the memcache example above) then I keep going.
It's rare because RPC & filesystem access is usually wrapped in a library or module which does not have any business knowledge of the task at hand. A more realistic example is your getUser() call which calls 10 stack frames down to a filesystem access, and let's say you get a filesystem error. The natural thing to do is throw an exception which goes up all 10 stack frames and gets caught by getUser(), which provides some sort of "sorry, internal error" message to the client.
The notion that typing "if error return error" through 10 stack frames makes you think more clearly is absurd. Decades of C experience has shown that lazy programmers will ignore critical error conditions and introduce hard-to-find bugs because execution plows ahead past the original error.
Whether getUser() returns an error object or throws an exception is a question of high-level API design. Sometimes UserNotExistsException makes sense, sometimes a null (or error) result makes sense. That is an entirely separate issue. Any designer with significant experience will use both approaches as appropriate.
Again, I'm not disagreeing that exceptions tend to be more terse. Exceptions optimize for writability at the expense of readability. Reading linear code that uses if statements and loops is easier than code that uses try-catches. Especially trying to come up with all the ways control could jump from happy path code to error-handling code.
You tend not to be writing all 10 methods in a particular call chain at the same time. You will be writing a few methods that call each other inside a module. You paint this as a massive timesink, and I can assure you, it definitely is not.
Lazy programmers can also have catch-all exception handlers. I don't see how exceptions help make lazy programmers perform due diligence.
That being said, there is a place for exceptions. Truly exceptional conditions such as index out of bounds, or nil pointer dereference, or some internal precondition violated, should be treated in an exceptional manner. Go does this with panics, and panics are almost never caught as part of control-flow. They tend to be caught at the root of goroutines, logged, and the goroutine killed. The HTTP library, for instance, will catch panics in any goroutine it spawns, and write a 503.
I just find it odd that people treat commonplace things as exceptional. File open failed? Could not resolve hostname? Broken TCP connection? These aren't particularly exceptional things. They are probably not a result of a bug, and so should be handled by the programmer.
We seem to be going around in circles here... You say 10 layers of if error return error is not a time sink, and I say it is. I spent about a decade doing C and C++ programming before Java, and IMHO exception handling is second only to garbage collection as life-changing language improvements.
There is a key difference here: When a lazy C or Go programmer fails to check an error value, execution continues - possibly many lines or stack frames ahead before some sort of failure symptom is observable. In perverse cases this can produce silent data corruption. I spent far too much of the 90s chasing down these kinds of problems.
When a lazy programmer uses a catch-all exception handler, the error is still caught - immediately - with the full stack trace of the original problem. This is golden. Furthermore, a catch-all exception handler that prints an error message to the user/http request/whatever is often exactly the right approach.
There's a lot of stupidity in the Java standard libraries, but your examples (file failure, bad hostname, broken connection) are exactly the kinds of things that should be exceptions, and are usually best caught at a high level where meaningful errors can be reported to the user.
Right, by typing a zillion if err != nil return err lines of code. In any sophisticated app, almost every single function call is going to require this boilerplate. Annoying.
Every Go article explains this, but it doesn't answer the higher level question of why not use exceptions. Even worse you are forced to return data values along with the error code/flag/object.
I've not seen a standard error object either which means every place that looks at an error has to be intimately aware of what it is looking at. There is an os.Error but that appears to be string only so there is no errno equivalent value inside.
In languages with exceptions there is a base error/exception/throwable that has reason fields as well as methods to get tracebacks which are extremely useful.
I'm now 85% convinced Go made a mistake not using exceptions.
If you're going to do error handling right in an exception-using language, you really need to catch every exception close to its source, otherwise you won't know how to handle it properly.
I think Go's main approach is this: errors are not exceptional. Errors happen all the time, and should be considered part of the normal control flow of the program, not something to be shoved into a hidden compartment and dealt with later.
Thus using the usual control flow statements to check for errors is entirely appropriate, and it's immediately obvious when reviewing the code if someone has failed to do the right error checking.
BTW there is no os.Error type any more. There's a language-defined "error" type, which is an interface containing a single method:
type error {
Error() string
}
You only need to be intimately aware of what you're looking at if you want to take some different action based on the kind of error. This is actually quite rare (usually you care only that something failed, not how it failed), but when you need to, generally the calling package defines an error type that represents a particular kind or class of errors (for example http://golang.org/pkg/os/#LinkError)
When you have exceptions you can decide how many levels of stack frames higher to handle it, and none of the intermediary functions have to be modified. With this mechanism, every intermediary must handle the error and must agree on the error type (to some degree).
The argument that keeps being trotted out for Go's approach is consistency of handling, which is a good thing. But it is very manual especially where there is distance between the code that finds an error and the code that decides what to do about it. Doing all that manual work doesn't seem to have any benefits to me.
An error isn't just about what went wrong - it's also about what you were trying to do when it went wrong, so you can do something appropriate. If you handle exceptions several stack frames up, then you lose that information. Doing things the Go way also means you can make nice error messages that reflect the task, rather than a stack trace that only makes sense if you know the source code.
Yes, every intermediary must agree on the error type - it's the language-defined error type, which defines a single method, Error, which returns the error as a string. There's no need for any further agreement.
Functions encapsulate errors. If I call a function, it is entirely up to me how I wish to handle that error. "The code that finds an error" is the code that calls the function. All the context is local. There's no need to know how that function first encountered that error - that's part of the implementation detail of that function. At every stage, we make error handling decisions based on local context. This makes the code more maintainable, because there is genuine separation of concerns.
If you really want exceptions (a classic example is a recursive descent parser where you don't want to check every call), you can use panic and recover, making sure that callers will never see it - it's a local contract only.
How do you lose information with exceptions? You can handle an exception in the code immediately surrounding whatever detects the problem and your code semantics are no different than Go. The Go mechanism doesn't give the option of handling it several stack frames higher without having to implement handling in every single intermediary function.
The arguments I keep seeing for Go's semantics seem to the same as the ones about manual memory allocation - you must be in control every step of the way.
It looks like panic only takes a string so it isn't a good equivalent to exceptions. Whatever gets flung around should generally have enough information to make decisions and to generate meaningful error messages.
Is there a particular reason why? I mean, if the failed query requires that a potentially quite involved operation needs to be aborted, an operation that is several functions deep, isn't panic exactly appropriate?
I do like exceptions in Python where their use is ubiquitous and there is garbage collection. I detest checked exceptions in Java because you are forced to handle things at a level you often don't want to.
I'm 65% certain Go made a mistake not using exceptions, but would love to hear from others.