| Something that annoys me: - Go's approach to normal error handling assumes that correctness and proper error handling are paramount: every error must be handled explicitly, and every control flow path must be made obvious, no matter how much verbosity this creates. - Go's approach to panic handling assumes that correctness and proper error handling don't matter: a function can halt at any given point, panics are easy to trigger by accident (e.g. methods with nil receivers), handling them explicitly is usually discouraged, and they tend to leave values in intermediate and unexpected states (unless you use "defer" very carefully and consistently). Rust partially resolves this issue by preventing many causes of panics, by using methods like "lock poisoning" to avoid leaving shared values in unexpected states, and by having proper destructors. Go's approach (just crash everything) makes it easy for one error to completely bring down an application. Handling panics leads to values being left in inconsistent states with operations half-completed. |
The reason I say you can’t turn every fault into an error return is because an unexpected infinite loop is also a type of fault. If you call some function that has bugs in it, then there’s a chance that the function won’t return. “Won’t return” may mean that it panics, deadlocks, loops forever, or loops for so long it might as well be forever. In Haskell, all of these different behaviors are lumped in together as “bottom”, and bottom is a value which has well-defined semantics even though it covers all these different cases. There’s a whole debate in the Haskell community about whether functions should be total. A total function doesn’t return bottom unless bottom was an argument—in Go/Rust terms, a total function does not panic and does not infinitely loop.
My take—as long as you think “this function might not return because it has a bug in it”, there’s not a good reason to prohibit panic(). The panic() functionality is a more controlled, flexible way for a function to not return.
I think you could write a whole article on when to use recover() in Go. The idea that you should never panic is a bit of a hopeless dream—if that’s the kind of correctness you want, then it sounds like you want some kind of formal verification, which can be done but not in Go. The idea that you should never recover() is too severe. Yes, you can find code that leaves your program in an inconsistent state after a panic(), but in practice I’d say that these problems are relatively rare. You can also use panic/recover to simplify your code in certain ways. I’ve used panic/recover to write parsers or deserialization code, where you just use a panic() to return an error from the top-level parser/deserializer, which catches it with a recover().
I’d also say that it’s relatively normal to recover() inside your request handler for network services. You could weigh the risk of panic/recover leaving your application server in an unexpected state against the risk of getting a denial of service from panic taking down the whole app.