Go (Golang) - Errors and panics

Published on 2019-07-16. Modified 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 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 perhaps have got some bad habits from another programming language.

Go is one of the few programming languages that by design forces you to thoroughly deal with errors. Go further splits problems into errors and panics, rather than just having exceptions like Java or Python, which is much better because an error is 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 be halted. 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 wrong input.

In a programming language like Java everything is an exception with the potential to crash you application, but because Java 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 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.

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).

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.

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

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

Some people believe that code like in the example above makes Go code verbose, having hundreds of if err != nil statements, and while you can wrap such code in order to make it less verbose, I fully believe that idiomatic Go 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 - and that is a really good thing!

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 think about errors when you program in another 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.

No! It does not fit into the philosophy of Go at all and "syntactic sugar" is just another word for being lazy. Furthermore, 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 can very easily and clearly see what's going on, and everything related to an error is something that "sticks out", something you pay attention to.

There is very little 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 because try has to be "unwrapped" and then another if block needs to be 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.

I personally really like the Go error and panic implementation and I wish people would stop constantly trying to change Go.

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