How to Error handling in GO

How to Error handling in GO

How to Error handling in GO - Error handling is the process of identifying when your program is in an unexpected state, and taking steps to record diagnostic information for later debugging.

Unlike other languages that require developers to handle errors with specialized syntax, errors in Go are values with the type error returned from functions like any other value. To handle errors in Go, we must examine these errors that functions could return, decide if an error has occurred, and take proper action to protect data and tell users or operators that the error occurred.

In this article, we will look at best practices that can be used to handle errors in the Go application.

The blank identifier

The blank identifier is an anonymous placeholder. It may be used like any other identifier in a declaration, but it does not introduce a binding. The blank identifier provides a way to ignore left-handed values in an assignment and avoid compiler errors about unused imports and variables in a program. The practice of assigning errors to the blank identifier instead of properly handling them is unsafe as this means you have decided to explicitly ignore the value of the defined function.

result, _ := iterate(x,y)

if value > 0 {
  // ensure you check for errors before results.
}

Your reason for probably doing this is that you’re not expecting an error from the function (or whatever error may occur) but this could create cascading effects in your program. The best thing to do is to handle an error whenever you can.

Handling errors through multiple return values

One way to handle errors is to take advantage of the fact that functions in Go support multiple return values. Thus you can pass an error variable alongside the result of the function you’re defining:

func iterate(x, y int) (int, error) {

}

In the code sample above, we have to return the predefined error variable if we think there’s a chance our function may fail. error is an interface type declared in Go’s built-in package and its zero value is nil.

type error interface {
   Error() string
 }

Usually, returning an error means there’s a problem and returning nil means there were no errors:

result, err := iterate(x, y)
 if err != nil {
  // handle the error appropriately
 } else {
  // you're good to go
 }

Thus whenever the function iterate is called and err is not equal to nil, the error returned should be handled appropriately – an option could be to create an instance of a retry or cleanup mechanism. The only drawback with handling errors this way is that there’s no enforcement from Go’s compiler, you have to decide on how the function you created returns the error. You can define an error struct and place it in the position of the returned values. One way to do this is by using the built-in errorString struct (you can also find this code at Go’s source code):

package errors

 func New(text string) error {
     return &errorString {
         text
     }
 }

 type errorString struct {
     s string
 }

 func(e * errorString) Error() string {
     return e.s
 }

In the code sample above, errorString embeds a string which is returned by the Error method. To create a custom error, you’ll have to define your error struct and use method sets to associate a function to your struct:

// Define an error struct
type CustomError struct {
    msg string
}
// Create a function Error() string and associate it to the struct.
func(error * CustomError) Error() string {
    return error.msg
}
// Then create an error object using MyError struct.
func CustomErrorInstance() error {
    return &CustomError {
        "File type not supported"
    }
}

The newly created custom error can then be restructured to use the built-in error struct:

import "errors"
func CustomeErrorInstance() error {
    return errors.New("File type not supported")
}

One limitation of the built-in error struct is that it does not come with stack traces. This makes locating where an error occurred very difficult. The error could pass through a number of functions before it gets printed out. To handle this, you could install the pkg/errors package which provides basic error handling primitives such as stack trace recording, error wrapping, unwrapping, and formatting. To install this package, run this command in your terminal:

go get github.com/pkg/errors

When you need to add stack traces or any other information that makes debugging easier to your errors, use the New or Errorf functions to provide errors that record your stack trace. Errorf implements the fmt.Formatter interface which lets you format your errors using the fmt package runes (%s, %v, %+v etc):

import(
    "github.com/pkg/errors"
    "fmt"
)
func X() error {
    return errors.Errorf("Could not write to file")
}

func customError() {
    return X()
}

func main() {
    fmt.Printf("Error: %+v", customError())
}

To print stack traces instead of a plain error message, you have to use %+v instead of %v in the format pattern, and the stack traces will look similar to the code sample below:

Error: Could not write to file
main.X
 /Users/raphaelugwu/Go/src/golangProject/error_handling.go:7
main.customError
 /Users/raphaelugwu/Go/src/golangProject/error_handling.go:15
main.main
 /Users/raphaelugwu/Go/src/golangProject/error_handling.go:19
runtime.main
 /usr/local/opt/go/libexec/src/runtime/proc.go:192
runtime.goexit
 /usr/local/opt/go/libexec/src/runtime/asm_amd64.s:2471

Defer, panic, and recover

Although Go doesn’t have exceptions, it has a similar kind of mechanism known as “Defer, panic, and recover“. Go’s ideology is that adding exceptions such as the try/catch/finally statement in JavaScript would result in complex code and encourage programmers to label too many basic errors, such as failing to open a file, as exceptional. You should not use defer/panic/recover as you would throw/catch/finally; only in cases of unexpected, unrecoverable failure.

Defer is a language mechanism that puts your function call into a stack. Each deferred function is executed in reverse order when the host function finishes regardless of whether a panic is called or not. The defer mechanism is very useful for cleaning up resources:

package main

import (
        "fmt"
)

func A() {
        defer fmt.Println("Keep calm!")
        B()
}
func B() {
        defer fmt.Println("Else...")
        C()
}
func C() {
        defer fmt.Println("Turn on the air conditioner...")
        D()
}
func D() {
        defer fmt.Println("If it's more than 30 degrees...")
}
func main() {
        A()
}

This would compile as:

If it's more than 30 degrees...
Turn on the air conditioner...
Else...
Keep calm!

Panic is a built-in function that stops the normal execution flow. When you call panic in your code, it means you’ve decided that your caller can’t solve the problem. Thus panic should only be used in rare cases where it’s not safe for your code or anyone integrating your code to continue at that point. Here’s a code sample depicting how panic works:

package main

import (
        "errors"
        "fmt"
)

func A() {
        defer fmt.Println("Then we can't save the earth!")
        B()
}
func B() {
        defer fmt.Println("And if it keeps getting hotter...")
        C()
}
func C() {
        defer fmt.Println("Turn on the air conditioner...")
        Break()
}
func Break() {
        defer fmt.Println("If it's more than 30 degrees...")
        panic(errors.New("Global Warming!!!"))

}
func main() {
        A()
}

The sample above would compile as:

If it's more than 30 degrees...
Turn on the air conditioner...
And if it keeps getting hotter...
Then we can't save the earth!
panic: Global Warming!!!

goroutine 1 [running]:
main.Break()
        /tmp/sandbox186240156/prog.go:22 +0xe0
main.C()
        /tmp/sandbox186240156/prog.go:18 +0xa0
main.B()
        /tmp/sandbox186240156/prog.go:14 +0xa0
main.A()
        /tmp/sandbox186240156/prog.go:10 +0xa0
main.main()
        /tmp/sandbox186240156/prog.go:26 +0x20

Program exited: status 2.

As shown above, when panic is used and not handled, the execution flow stops, all deferred functions are executed in reverse order and stack traces are printed.

You can use the recover built-in function to handle panic and return the values passing from a panic call. recover must always be called in a defer function else it will return nil:

package main

import (
        "errors"
        "fmt"
)

func A() {
        defer fmt.Println("Then we can't save the earth!")
        defer func() {
                if x := recover(); x != nil {
                        fmt.Printf("Panic: %+v\n", x)
                }
        }()
        B()
}
func B() {
        defer fmt.Println("And if it keeps getting hotter...")
        C()
}
func C() {
        defer fmt.Println("Turn on the air conditioner...")
        Break()
}
func Break() {
        defer fmt.Println("If it's more than 30 degrees...")
        panic(errors.New("Global Warming!!!"))

}
func main() {
        A()
}

As can be seen in the code sample above, recover prevents the entire execution flow from coming to a halt because we threw in a panic function and the compiler would return:

If it's more than 30 degrees...
Turn on the air conditioner...
And if it keeps getting hotter...
Panic: Global Warming!!!
Then we can't save the earth!

Program exited.

To report an error as a return value, you have to call the recover function in the same goroutine as the panic function is called, retrieve an error struct from the recover function, and pass it to a variable:

package main

import (
        "errors"
        "fmt"
)

func saveEarth() (err error) {

        defer func() {
                if r := recover(); r != nil {
                        err = r.(error)
                }
        }()
        TooLate()
        return
}
func TooLate() {
        A()
        panic(errors.New("Then there's nothing we can do"))
}

func A() {
        defer fmt.Println("If it's more than 100 degrees...")
}
func main() {
        err := saveEarth()
        fmt.Println(err)
}

Every deferred function will be executed after a function call but before a return statement. So, you can set a returned variable before a return statement gets executed. The code sample above would compile as:

If it's more than 100 degrees...
Then there's nothing we can do

Program exited.

Error wrapping

Previously error wrapping in Go was only accessible via using packages such as pkg/errors. However, with Go’s latest release – version 1.13, support for error wrapping is present. According to the release notes:

An error e can wrap another error w by providing an Unwrap method that returns w. Both e and w are available to programs, allowing e to provide additional context to w or to reinterpret it while still allowing programs to make decisions based on w.

To create wrapped errors, fmt.Errorf now has a%wverb and for inspecting and unwrapping errors, a couple of functions have been added to the error package:

errors.Unwrap: This function basically inspects and exposes the underlying errors in a program. It returns the result of calling the Unwrap method on Err. If Err’s type contains an Unwrap method returning an error. Otherwise, Unwrap returns nil.

package errors

type Wrapper interface{
  Unwrap() error
}

Below is an example implementation of the Unwrap method:

func(e*PathError)Unwrap()error{
  return e.Err
}

errors.Is: With this function, you can compare an error value against the sentinel value. What makes this function different from our usual error checks is that instead of comparing the sentinel value to one error, it compares it to every error in the error chain. It also implements an Is method on an error so that an error can post itself as a sentinel even though it’s not a sentinel value.

func Is(err, target error) bool

In the basic implementation above, Is checks and reports if err or any of the errors in its chain are equal to target (sentinel value).

errors.As: This function provides a way to cast to a specific error type. It looks for the first error in the error chain that matches the sentinel value and if found, sets the sentinel value to that error value and returns true:

package main

import (
        "errors"
        "fmt"
        "os"
)

func main() {
        if _, err := os.Open("non-existing"); err != nil {
                var pathError *os.PathError
                if errors.As(err, &pathError) {
                        fmt.Println("Failed at path:", pathError.Path)
                } else {
                        fmt.Println(err)
                }
        }

}

You can find this code in Go’s source code.

Compiler result:

Failed at path: non-existing

Program exited.

An error matches the sentinel value if the error’s concrete value is assignable to the value pointed to by the sentinel value. As will panic if the sentinel value is not a non-nil pointer to either a type that implements error or to any interface type. As returns false if err is ```nil.``

Learn More

Thanks for reading !

go

Bootstrap 5 Complete Course with Examples

Bootstrap 5 Tutorial - Bootstrap 5 Crash Course for Beginners

Nest.JS Tutorial for Beginners

Hello Vue 3: A First Look at Vue 3 and the Composition API

Building a simple Applications with Vue 3

Deno Crash Course: Explore Deno and Create a full REST API with Deno

How to Build a Real-time Chat App with Deno and WebSockets

Convert HTML to Markdown Online

HTML entity encoder decoder Online

What's new in the go 1.15

Go announced Go 1.15 version on 11 Aug 2020. Highlighted updates and features include Substantial improvements to the Go linker, Improved allocation for small objects at high core counts, X.509 CommonName deprecation, GOPROXY supports skipping proxies that return errors, New embedded tzdata package, Several Core Library improvements and more.

Tiny Go: Small Is Going Big

Ron Evans talks about TinyGo - a compiler for Go, written in Go itself, that uses LLVM to achieve very small, fast, and concurrent binaries that can also target devices where Go could never go before.

Secure HTTPS servers in Go

In this article, we are going to look at some of the basic APIs of the http package to create and initialize HTTPS servers in Go

Go Go Release!

TLDR; Just Want A Go Release Tool? I’m neither a lover nor a hater when it comes to Go and its ecosystem. Generally things Go are a bit rudimentary but very open and accessible. You may have to do some work yourself, but you’ll be able to get results.