Go's error handling leaves something to be desired. I find myself reinventing exceptions a lot by just passing errors up through generic "error" return types.
It feels like it would be a lot cleaner to add syntax support for exceptions and call it a day.
There is support for fully "exceptional" behavior. It's called "panic".
The Go language designers explicitly didn't include exceptions because "coupling exceptions to a control structure, as in the try-catch-finally idiom, results in convoluted code. It also tends to encourage programmers to label too many ordinary errors, such as failing to open a file, as exceptional" [1].
I agree with them. While handling errors everywhere is a little painful to write, it enforces better practices by making you acknowledge that things could fail and ignore, handle, or pass the buck.
> I agree with them. While handling errors everywhere is a little painful to write, it enforces better practices by making you acknowledge that things could fail and ignore, handle, or pass the buck.
There are better ways of accomplishing the same thing though, one way is the Either monad in Haskell. Here's an example I post sometimes comparing error handling in Go to Haskell's either monad:
func failureExample()(*http.Response) {
// 1st get, do nothing if success else print exception and exit
response, err := http.Get("http://httpbin.org/status/200")
if err != nil {
fmt.Printf("%s", err)
os.Exit(1)
} else {
defer response.Body.Close()
}
// 2nd get, do nothing if success else print exception and exit
response2, err := http.Get("http://httpbin.org/status/200")
if err != nil {
fmt.Printf("%s", err)
os.Exit(1)
} else {
defer response2.Body.Close()
}
// 3rd get, do nothing if success else print exception and exit
response3, err := http.Get("http://httpbin.org/status/200")
if err != nil {
fmt.Printf("%s", err)
os.Exit(1)
} else {
defer response3.Body.Close()
}
// 4th get, return response if success else print exception and exit
response4, err := http.Get("http://httpbin.org/status/404")
if err != nil {
fmt.Printf("%s", err)
os.Exit(1)
} else {
defer response4.Body.Close()
}
return response4
}
func main() {
fmt.Println("A failure.")
failure := failureExample();
fmt.Println(failure);
}
The equivalent Haskell code:
failureExample :: IO (Either SomeException (Response LBS.ByteString))
failureExample = try $ do
get "http://www.httpbin.org/status/200"
get "http://www.httpbin.org/status/200"
get "http://www.httpbin.org/status/200"
get "http://www.httpbin.org/status/404"
main = failureExample >>= \case
Right r -> putStrLn $ "The successful pages status was (spoiler: it's 200!): " ++ show (r ^. responseStatus)
Left e -> putStrLn ("error: " ++ show e)
I took a survey of my Go code base once. In the mature bits of code, approximately 1/3rd of the places where an error was received, something other than simply returning it was done. And I don't really go in for crazy wrapping schemes, either... it means something was logged, or retried, or modified, or somehow reacted to.
What often starts out as simply a return, by the time something gets to production quality, has often changed.
If you're using Go for its core use case, network servers, the error handling turns out to be very solid, precisely because almost every other error handling paradigm strongly encourages you to just lump all the errors together and not think about them individually. (Yes, that includes Option<>.) I can see where that might be very annoying on a desktop GUI app or something, but if you are not thinking about every single error in your at-scale network server, you're actually doing it wrong.
Why? Seems to me that it's precisely equivalent to Golang's error handling story (assuming you mean something like Result). In fact, it's a lot easier to handle errors individually that way, both because it tends to make it harder to use a return value without checking for errors and because its generic nature means that you can handle different errors differently without having to deal with interfaces.
If you mean that it's more typing to write "return err" and it forces you to think about it more, I don't really buy that. "return err" is 10 characters; "map" (e.g. in Haskell) is 3 and "try!()" is 6. I really doubt that the difference between 3 and 10 characters has any practical outcome. For Golang programmers, "return err" is muscle memory.
I should have said the monadic form of Option<>. When you use it monadically, the result is nearly equivalent to exception handling; you call a function and the default behavior is to bundle up the error and just throw it up. It is true that if you are manually unwrapping it every single time, it's equivalent to checking every time.
"For Golang programmers, "return err" is muscle memory."
First, as I said, no, I actually think about it every time. And second, I bet you end up with "muscle memory" default handling under any scheme (for instance, exceptions: don't catch them or rethrow them)... in the end, you can bring the horse to water but you can't make it drink. You can only feel good that at least you brought it and you did your part. Option<>, even manually unwrapped, does not force the programmer to do something sensible with the error any more than any thing else does, or indeed, can.
It feels like it would be a lot cleaner to add syntax support for exceptions and call it a day.