Skip to main content

Error Handling

In Go, there is a built-in error type. The different values of error type indicate an abnormal state. Usually in Go, if the error value is not nil then an error has occurred. It must be dealt with in order to allow the application to recover from that state without crashing.

A simple example taken from the Go blog follows:

if err != nil {
// handle the error
}

Not only can the built-in errors be used, we can also specify our own error types. This can be achieved by using the errors.New function. Example:

{...}
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
//If an error has occurred print it
if err != nil {
fmt.Println(err)
}
{...}

If we need to format the string containing the invalid argument to see what caused the error, the Errorf function in the fmt package allows us to do this.

{...}
if f < 0 {
return 0, fmt.Errorf("math: square root of negative number %g", f)
}
{...}

When dealing with error logs, developers should ensure no sensitive information is disclosed in the error responses, as well as guarantee that no error handlers leak information (e.g. debugging, or stack trace information).

In Go, there are additional error handling functions, these functions are panic, recover and defer. When an application state is panic its normal execution is interrupted, any defer statements are executed, and then the function returns to its caller. recover is usually used inside defer statements and allows the application to regain control over a panicking routine, and return to normal execution. The following snippet, based on the Go documentation explains the execution flow:

func main () {
start()
fmt.Println("Returned normally from start().")
}

func start () {
defer func () {
if r := recover(); r != nil {
fmt.Println("Recovered in start()")
}
}()
fmt.Println("Called start()")
part2(0)
fmt.Println("Returned normally from part2().")
}

func part2 (i int) {
if i > 0 {
fmt.Println("Panicking in part2()!")
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("Defer in part2()")
fmt.Println("Executing part2()")
part2(i + 1)
}

Output:

Called start()
Executing part2()
Panicking in part2()!
Defer in part2()
Recovered in start()
Returned normally from start().

By examining the output, we can see how Go can handle panic situations and recover from them, allowing the application to resume its normal state. These functions allow for a graceful recovery from an otherwise unrecoverable failure.

It is worth noting that defer usages also include Mutex Unlocking, or loading content after the surrounding function has executed (e.g. footer).

In the log package there is also a log.Fatal. Fatal level is effectively logging the message, then calling os.Exit(1). Which means:

  • Defer statements will not be executed.
  • Buffers will not be flushed.
  • Temporary files and directories are not removed.

Considering all the previously mentioned points, we can see how log.Fatal differs from Panic and why it should be used carefully. Some examples of the possible usage of log.Fatal are:

  • Set up logging and check whether we have a healthy environment and parameters. If we don't, then there's no need to execute our main().
  • An error that should never occur and that we know that it's unrecoverable.
  • If a non-interactive process encounters an error and cannot complete, there is no way to notify the user about this error. It's best to stop the execution before additional problems can emerge from this failure.

To demonstrate, here's an example of an initialization failure:

func initialize(i int) {
...
//This is just to deliberately crash the function.
if i < 2 {
fmt.Printf("Var %d - initialized\n", i)
} else {
//This was never supposed to happen, so we'll terminate our program.
log.Fatal("Init failure - Terminating.")
}
}

func main() {
i := 1
for i < 3 {
initialize(i)
i++
}
fmt.Println("Initialized all variables successfully")
}

It's important to assure that in case of an error associated with the security controls, its access is denied by default.