DEV Community

Cover image for Go Course: Errors
Karan Pratap Singh
Karan Pratap Singh

Posted on • Originally published at karanpratapsingh.com

Go Course: Errors

In this tutorial, let's talk about error handling.

Notice I said errors and not exceptions as there is no exception handling in Go.

Instead, we can just return a built-in error type which is an interface type.

type error interface {
    Error() string
}
Enter fullscreen mode Exit fullscreen mode

We will circle back to this shortly. First, let's try to understand the basics.

So, let's declare a simple Divide function which, as the name suggests,, will divide integer a by b.

func Divide(a, b int) int {
    return a/b
}
Enter fullscreen mode Exit fullscreen mode

Great. Now, we want to return an error, let's say, to prevent the division by zero. This brings us to error construction.

Constructing Errors

There are multiple ways to do this, but we will look at the two most common one.

errors package

The first is by using the New function provided by the errors package.

package main

import "errors"

func main() {}

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("cannot divide by zero")
    }

    return a/b, nil
}
Enter fullscreen mode Exit fullscreen mode

Notice, how we return error with the result. And if there is no error we simply return nil as it is the zero value of an error because after all, it's an interface.

But how do we handle it? So, for that, let's call the Divide function in our main function.

package main

import (
    "errors"
    "fmt"
)

func main() {
    result, err := Divide(4, 0)

    if err != nil {
        fmt.Println(err)
        // Do something with the error
        return
    }

    fmt.Println(result)
    // Use the result
}

func Divide(a, b int) (int, error) {...}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
cannot divide by zero
Enter fullscreen mode Exit fullscreen mode

As you can see, we simply check if the error is nil and build our logic accordingly. This is considered quite idiomatic in Go and you will see this being used a lot.

Another way to construct our errors is by using the fmt.Errorf function.

This function is similar to fmt.Sprintf and it lets us format our error. But instead of returning a string, it returns an error.

It is often used to add some context or detail to our errors.

...
func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("cannot divide %d by zero", a)
    }

    return a/b, nil
}
Enter fullscreen mode Exit fullscreen mode

And it should work similarly.

$ go run main.go
cannot divide 4 by zero
Enter fullscreen mode Exit fullscreen mode

Sentinel Errors

Another important technique in Go is defining expected Errors so they can be checked explicitly in other parts of the code. These are sometimes referred to as sentinel errors.

package main

import (
    "errors"
    "fmt"
)

var ErrDivideByZero = errors.New("cannot divide by zero")

func main() {...}

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, ErrDivideByZero
    }

    return a/b, nil
}
Enter fullscreen mode Exit fullscreen mode

In Go, it is considered conventional to prefix the variable with Err. For example, ErrNotFound.

But what's the point?

So, this becomes useful when we need to execute a different branch of code if a certain kind of error is encountered.

For example, now we can check explicitly which error occurred using the errors.Is function.

package main

import (
    "errors"
    "fmt"
)

func main() {
    result, err := Divide(4, 0)

    if err != nil {
        switch {
    case errors.Is(err, ErrDivideByZero):
        fmt.Println(err)
                // Do something with the error
    default:
        fmt.Println("no idea!")
    }

        return
    }

    fmt.Println(result)
    // Use the result
}

func Divide(a, b int) (int, error) {...}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
cannot divide by zero
Enter fullscreen mode Exit fullscreen mode

Custom Errors

This strategy covers most of the error handling use cases. But sometimes we need additional functionalities such as dynamic values inside of our errors.

Earlier, we saw that error is just an interface. So basically, anything can be an error as long as it implements the Error() method which returns an error message as a string.

So, let's define our custom DivisionError struct which will contain an error code and a message.

package main

import (
    "errors"
    "fmt"
)

type DivisionError struct {
    Code int
    Msg  string
}

func (d DivisionError) Error() string {
    return fmt.Sprintf("code %d: %s", d.Code, d.Msg)
}

func main() {...}

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, DivisionError{
            Code: 2000,
            Msg:  "cannot divide by zero",
        }
    }

    return a/b, nil
}
Enter fullscreen mode Exit fullscreen mode

Here, we will use errors.As instead of errors.Is function to convert the error to the correct type.

func main() {
    result, err := Divide(4, 0)

    if err != nil {
        var divErr DivisionError

        switch {
        case errors.As(err, &divErr):
            fmt.Println(divErr)
            // Do something with the error
        default:
            fmt.Println("no idea!")
        }

        return
    }

    fmt.Println(result)
    // Use the result
}

func Divide(a, b int) (int, error) {...}
Enter fullscreen mode Exit fullscreen mode
$ go run man.go
code 2000: cannot divide by zero
Enter fullscreen mode Exit fullscreen mode

But what's the difference between errors.Is and errors.As?

The difference is that this function checks whether the error has a specific type, unlike the Is(), which examines if it is a particular error object.

We can also use type assertions but it's not preferred.

func main() {
    result, err := Divide(4, 0)

    if e, ok := err.(DivisionError); ok {
        fmt.Println(e.Code, e.Msg) // Output: 2000 cannot divide by zero
        return
    }

    fmt.Println(result)
}
Enter fullscreen mode Exit fullscreen mode

Lastly, I will say that error handling in Go is quite different compared to the traditional try/catch idiom in other languages. But it is very powerful as it encourages the developer to actually handle the error in an explicit way, which improves readability as well.


This article is part of my open source Go Course available on Github.

GitHub logo karanpratapsingh / learn-go

Master the fundamentals and advanced features of the Go programming language

Learn Go

Hey, welcome to the course, and thanks for learning Go. I hope this course provides a great learning experience.

This course is also available on my website and as an ebook on leanpub. Please leave a ⭐ as motivation if this was helpful!

Table of contents

What is Go?

Go (also known as Golang) is a programming language developed at Google in 2007 and open-sourced in 2009.

It focuses on simplicity, reliability, and efficiency. It was designed to combine the efficacy, speed…

Oldest comments (0)