Go (Golang) - Errors and panics

Posted on 2019-07-16. Last updated on 2019-07-17.
In Go you have to deal with both errors and panics and while some people consider it one of the worst things about Go, I think it demonstrates exactly how well designed and carefully crafted Go is.

The Go error and panic system is a carefully crafted system that forces the developer into a really good set of programming habits, and I think that the only way you can be unhappy about the Go error and panic design is if you have perhaps gotten some bad habits from another programming language.

Go is perhaps the only programming language that forces you to deal with errors and panics so thoroughly and it is by design, not by mistake. And splitting problems into errors and panics, rather than just having exceptions like Java or Python, is much better because an error is really not an exception.

In Go a panic is described as:

A built-in function that stops the ordinary flow of control and begins panicking. When the function F calls panic, execution of F stops, any deferred functions in F are executed normally, and then F returns to its caller. To the caller, F then behaves like a call to panic. The process continues up the stack until all functions in the current goroutine have returned, at which point the program crashes. Panics can be initiated by invoking panic directly. They can also be caused by runtime errors, such as out-of-bounds array accesses.

This means that when you encounter a panic your application execution is halted and you are forced to plan how to deal with such issues in a graceful manner by recovering. If you don't plan and prepare for panics, your application will simply crash.

An error on the other hand is something else. An error in Go is simply a conventional interface that represents an error condition of some kind, with the nil value representing no error. An error is not something that can cause a crash and the execution isn't halted, and it is not supposed to. An error is simply a representation of something that has failed in its capacity during the normal flow of operations.

Say you have a function that is supposed to add two numbers but someone accidentally feeds it a string instead of a number. A problem like this is not supposed to cause your application to crash, however you need to plan ahead and think carefully about what might go wrong with your function if someone feeds it useless input.

In a programming language like Java everything is an exception with the potential to crash you application, but because the Java programming language does not require methods to catch or to specify unchecked exceptions (RuntimeException, Error, and their subclasses), programmers often write code that only throws unchecked exceptions or make all their exception inherit from the RuntimeException. This means that Java allows programmers to write code without bothering with compiler errors and without bothering to specify or to catch any exceptions.

You can be lazy about errors in Go too and just ignore them, but this is not idiomatic Go code. Instead Go tries hard to make you consider the return value of every function in which an error of some kind might occur and by dividing problems into errors and panics you are forced to deal with such issues separately, which is a really good thing.

Go has multivalue returns that makes it easy to return a detailed error description alongside the normal return value and it is considered a good coding style to use this feature and always provide error information.

If we take a look at the os.Open function, as illustrated in the Effective Go document, the function returns an error value that describes what went wrong in case it cannot open a file. The error provides detailed information about why the function failed and the operating system error that triggered it.

Error handling is important. The language's design and conventions encourage you to explicitly check for errors where they occur (as distinct from the convention in other languages of throwing exceptions and sometimes catching them).

foo, err := oneThing()
if err != nil {
    // Handle the issue.
}

bar, err := anotherThing()
if err != nil {
    // Handle the issue.
}

Some people believe that code like this makes Go code verbose, having hundreds of ìf err statements, and while you can wrap such code in order to make it less verbose, I fully believe that this not only makes the code more readable, but it also accelerates and cultivates a way of thinking in which errors are constantly present in your mind when you program. This so called "verbosity" is not something that should be avoided, rather it should be embraced as it makes you a better and more attentive programmer.

Just think about how often you use to think about errors when you program in a different programming language.

Unfortunately some people in the Go community has proposed a built-in Go "try" function. And while this isn't a true try-and-catch function like in Java, it is a big step in the wrong direction in my humble opinion.

The idea is that

f, err := os.Open(filename)
if err != nil {
    return …, err  // zero values for other results, if any
}

can be "simplified" to

f := try(os.Open(filename))

The proposal states:

In summary, try may seem unusual at first, but it is simply syntactic sugar tailor-made for one specific task, error handling with less boilerplate, and to handle that task well enough. As such it fits nicely into the philosophy of Go. try is not designed to address all error handling situations; it is designed to handle the most common case well, to keep the design simple and clear.

But no, it does not fit into the philosophy of Go at all! "Syntactic sugar" is another word for being lazy and the proposed try function does not even make the code more readable, rather it is moving a vertical statement into a horizontal statement that makes the code less readable.

In the traditional if statement, you not only very easily see what's going on, but everything about errors becomes something that sticks out, something you really notice and pay much attention to.

There isn't much difference between

f := try(os.Open(filename))

and then

f, - := os.Open(filename)

In which case the error is just ignored.

Another problem with this solution is that it adds complexity to debugging in which try has to be "unwrapped" and then an if block added, then again re-wrapped in try when done debugging. Dealing with errors immediately after each function call is critical to having code that can be easily debugged.

No, Robert Griesemer, Rob Pike, and Ken Thompson knew what they where doing when they designed Go. I love the Go error and panic implementation and I would be extremely resistant to import any kind of code that would implements the try function if it ever finds its way into Go.

If it ain't broke, don't fix it!

Update 2019-07-17: The try proposal has just been declined. It is further discussed on Hacker News.