Go Error Handling with Panic, Recovery, and Defer

Go Error Handling with Panic, Recovery, and Defer

Normal errors in code are easily anticipated in most instances, but panics fall into the category of unanticipated errors. We can defer the function call to execute panics anyway, but there are better methods to handle them, as we will showcase in this Go developer tutorial.

Panics cause a program to terminate abruptly, therefore developers should use the recovery function to gain control of it and clean up some of the mess before the program quits. This article describes these concepts with examples using the Go programming language.

New to error handling in Go?

Handling Panics in Go or Golang It is common to have errors or bugs in code. While really well-written code can claim to have fewer bugs the truth is, even the best code is rarely error-free. Additionally, errors can creep into code during the addition of features, modification of code, refactoring, and updating process. Minor errors produce glitches during program execution but may not always crash the system. That, however, does not mean they are less serious. In fact, those types of issues can be more dangerous in the long run if not eradicated.

It is not easy to write error-free code; a good program is actually the result of a continuous process of coding and debugging. Good coders are aware of this and handle errors gracefully. The errors that can be anticipated are immediately taken care of by the programmer.

The type system of Go is equipped to detect many types of errors at compile time and will flag an error message during compilation if one is found. However, there are errors that are unanticipated and can only be detected at run-time. For example, errors coming from one function or method calling another with invalid inputs can only be detected by executing the code. Different programming languages use different techniques to handle these types of errors. For instance, in C, programmers are responsible for validating every input and checking return values. Java, meanwhile, can defer error handling using exceptions. Go does not have a single solution, but instead, works on the pattern of error occurrences and handles them at the appropriate time.

How to use the Defer Statement in Go There is always a gap between the occurrence of an error and the place where provisions for error handling are written. Developers must ensure that code is safe even in this in-between, critical area. The presence or absence of an error should not be able to break code and the code should execute in spite of any errors that might occur.

In C++, such a situation may be handled by a local destructor. In Java, it is handled with the finally statement. In Go, we use defer. The function call in the defer statement occurs after the function exits. This mechanism can be effectively used to release critical system resources – such as closing an open file – to ensure that our code does not leak file descriptors.

A defer statement is an ordinary function call prefixed by the defer keyword. Although the deferred function argument expressions are evaluated during the execution of the statement, the actual call is deferred until the function containing the defer statement completes its execution, with or without an error. If there is more than one deferred statement; they are executed in the reverse order in which they were deferred.

Typically, defer statements are paired with the open and close, connect and disconnect, or lock and unlock of resources to ensure that the resources are gracefully released, irrespective of the occurrence of an error. Here is an example of defer in Go:

package main

import (
    "fmt"
    "os"
)

func openOrCreateFile(fileName string) *os.File 
    file, err := os.Create(fileName)
    if err != nil 
        panic(err)
     else 
        return file

func closeFile(file *os.File) 
    err := file.Close()
    if err != nil 
        fmt.Fprintf(os.Stderr, "Error! %vn", err)
        os.Exit(1)

func writeToFile(file *os.File) 
    fmt.Fprintln(file, "This is a sample text.")

func main() 

    fileName := "sample.txt"
    file := openOrCreateFile(fileName)
    defer closeFile(file)
    writeToFile(file)

Panic in Go The Go programming language does not have an exception in the same sense as other languages; however, it does offer a similar idea (semantically) with panic calls. The type system in Go catches several errors during compilation but there are errors, such as accessing an array out of bounds or nil pointer dereference, that can only be detected at run-time. When Golang encounters such an error it panics.

The panic causes the normal execution of the program to stop but all of the deferred function calls are executed before it crashes with a log message. The log message provides the reason for the panic and contains the panic value, error message, stack trace, and, in short, all of the information required to diagnose the cause of the problem. In the sample Go code below, we will create a quick out-of-bound panic:

package main

import "fmt"

func main() 

    deck := []string"Spade", "Club", "Heart", "Diamond"
    fmt.Println("This will cause panic", deck[len(deck)])

The above code results in the following output when you run it in your IDE or code editor:

panic: runtime error: index out of range [4] with length 4
goroutine 1 [running]:
main.main()
        /home/mano/Documents/go_workspace/proj1/main.go:8 +0x1b

Note that the name “index out of range” provides the hint of the error. The Go compiler has no idea of the value returned by the function len() or the value it returns until the code is executed. As a result, the compiler cannot catch the error during compilation. If such an error exists in the code, the runtime system has no other option but to stop execution. The panic message provides a clue for developers to rectify the problem.

Interestingly, there is a built-in panic function. This means we can make our panic calls. The built-in panic function can be called directly. It accepts a string value as an argument. This flexibility is provided to the programmer who can choose their own set of panics and rant on the ramp of their own code. This is particularly useful in a situation where creating our own custom package allows us to tell the user that they made certain mistakes. In short, it lets developers set the rules for their own panic.

Here is a code snippet showing how the panic function can be used. Note that we also used the panic function in our first example:

switch day := weekDay(chooseWeekDay()); s 
    case "Sunday":
    // ...
    case "Wednesday":
    // ...
    case "Friday": // ...
    case "Saturday":
    // ...
    default:
    panic(fmt.Sprintf("invalid weekday %q", s)) // panic!

The call to the panic function causes the program to crash and should be used only under grave circumstances. It is not a good idea to use panic on every error; rather, try to handle errors gracefully, using other methods. That being said, using panics are fine under extreme circumstances.

Recovery from Panic in Go Panic means the error wins and the program quits execution. Sometimes, however, it is possible to recover, such as by performing some cleanup before the crash. For example, if an unexpected situation occurs during the database connection, we may be able to report to the client that the program has quit abruptly due to a connection error rather than hanging around. This simply means that we can handle panics as well as errors. There is a built-in function called recover that allows developers to intercept a panic through the call stack and prevent the program from abrupt termination.

Here is an example showing how to use the recover function in Go:

package main

import (
    "fmt"
    "log"
)

func main() 
    divByZero()
    fmt.Println("Although panicked. We recovered. We call mul() func")
    fmt.Println("mul func result: ", mul(5, 10))

func div(x, y int) int 
    return x / y

func mul(x, y int) int 
    return x * y

func divByZero() 
    defer func() 
        if err := recover(); err != nil 
            log.Println("panic occurred:", err)

    ()
    fmt.Println(div(1, 0))

The above code results in the following output:

2021/08/24 19:13:55 panic occurred: runtime error: integer divide by zero
Although panicked. We recovered. We call mul() func
mul func result:  50

A well-written program exits on a panic. That is a normal response. We are able to recover only in some select cases. In fact, recovery after a panic is not always a good idea and must be used with caution. Panics only occur when something has gone wrong. Attempting to resume the execution of an already erroneous program is not a very good idea. This may result in unexpected behaviors within the program. It is easy to decide when to use panic but very difficult to decide when to recover. The mechanism of recovery exists in Go but should be used with extreme caution.

Error Handling using Go Final Thoughts In this Go tutorial, we have explained the defer, panic, and recovery mechanisms that Golang has to offer. They are an important part of the error handling process in Go. The defer statements are the most common and quite often used during error handling, while recovery is used rarely and very cautiously. Explicitly calling a panic function is also required under rare circumstances. It is extremely important to understand these three error-handling mechanisms to write production-ready code in Go.