You’re just wrong.
I beg to differ. Here are some notes from Exceptional Ruby which cites the historical literature on the subject.
if a library panics because a file is missing, I’m going to recover from that
Key term: “library”. Of course you need to wrap your library at the top level in some minimal exception catching that presents everything cleanly to the client.
If my own application encounters an exceptional situation, that by definition means it can’t be sensibly recovered from (or I don’t think it’s worth the cost to add complexity, i.e. “code”, to make it recoverable). I would much rather crash than just swallow exceptions and keep running and possibly do more damage.
It’s absolutely nonsense to basically crash your application simply because you can’t perform 100% of the capabilities.
The whole point of “exception” is that it’s not something that you can recover from. If you can recover, it’s not an exception. There are caveats and flexibility around the idea of “application”. If my web request reaches an exception, I’d much rather crash than continue and possibly corrupt data or give the user the impression something succeeded that didn’t. But that doesn’t mean I want the whole server to crash. Maybe I just want the current request to crash. I can then isolate that branch with a rescue/recover, as a Rails web application would do by default for instance.
But suppose I can’t load some file that stores some critical application wide configuration. I just want to crash the entire application and surface this immediately. The idea of letting my application boot knowing that it’s going to be badly broken, deferring someone catching the problem, is not pragmatic.
I speak from an experience with like 20 different San Francisco startup codebases, e.g. being the 11th engineer at Zendesk (now a 3.5 billion dollar company). I have witnessed the pain of people rescuing exceptions and hiding bugs. On apps I’ve greenfielded, I’ve pretty much banned the use of
rescue. This works better. I’ve seen the results over many years.
Have you heard of tools like Airbrake, Rollbar, and Sentry? These surface exceptions in a searchable organized way, storing stack traces, HTTP headers, etc.
One problem I see is that the semantics of “exception” and “error” are not programmatically enforced, and arguably cannot be. And thus, the distinction might cause more harm than benefit.
E.g. Ruby has
raise (aliased as
rescue for exceptions. It also has
catch, which behave basically the same way “unwinding the stack”—but are semantically used for “control flow” not “exceptions”. I’ve NEVER seen someone use throw/catch in my 12 years of Ruby on Rails experience at a zillion startups (if I include my consulting). And even if my library does something “exceptional”, there’s the fact that—as you yourself say—I want to catch that and surface it to the client as an error. But all that really means in practice is using a different mechanism—accepting a return value instead rescuing from an exception. This is purely a matter of convention. Indeed, the Ruby community could decide that both should be treated the same way, and the client should just use a
rescue, even for “control flow”.
Indeed, the standard libraries even create some ambiguity here. Consider
#accept_nonblock method. The docs say: “By specifying `exception: false`, the options hash allows you to indicate that
#accept_nonblock should not raise an
IO::WaitWritable exception, but return the symbol
:wait_writable instead.” This is kind of like returning an error vs. calling
panic in Go. But by default, this library can raise an exception, and the accepted way to deal with this is for the client code to rescue the exception.
And even Golang uses exceptions (
panic) for control flow.
For a real-world example of panic and recover, see the json package from the Go standard library. It decodes JSON-encoded data with a set of recursive functions. When malformed JSON is encountered, the parser calls panic to unwind the stack to the top-level function call, which recovers from the panic and returns an appropriate error value (see the ‘error’ and ‘unmarshal’ methods of the decodeState type in decode.go).
The convention in the Go libraries is that even when a package uses panic internally, its external API still presents explicit error return values.
So it seems that the choice between “unwinding the stack” and “returning an error” is effectively just structural/logistical. But it’s advised to return an error rather than panic to any caller outside your package (i.e. in any public function). It’s as if to say, “making someone use recover from a call to your library is ugly, so be nice and return an error instead”.
Perhaps the entire distinction between errors and exceptions should just go away.