3 ms·
We have two orthogonal tasks which traditional exception mechanisms complect together: 1. Handling a failed assumption, which will cause the code that follows
by taliesinb 14y ago
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.
- stickfigure 14y agoWhy 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.
- agentS 14y agoWhy 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.
- stickfigure 14y agoIt'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.
- agentS 14y agoAgain, 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.