Error Handling in Go

Error Handling in Go

In this tutorial, we are going to discuss error handling in the Go language. Go language does not provide a conventional try/catch method to handle the errors. Instead, errors are returned as a normal return value.

Error Handling in Go
What are errors?

Errors indicate an abnormal condition occurring in the program.

Let’s say we are trying to open a file, and the file does not exist in the file system. This is an abnormal condition, and it’s represented as an error.

Errors in Go are plain old values. Errors are represented using the built-in error type. We will learn more about the error type later in this tutorial.

Just like any other built-in type such as int, float64, etc., error values can be stored in variables, passed as parameters to functions, returned from functions, and so on.

Let’s stop the theory and write some code right away with an example program that tries to open a file that does not exist.

package main

import (  
    "fmt"
    "os"
)

func main() {  
    f, err := os.Open("/data.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(f.Name(), "opened successfully..!!")
}

Output

open /data.txt: no such file or directory

Run in playground

In the above program, we try to open the file at path /data.txt (which will not exist in the playground). The Open function of the os package has the following signature,

func Open(name string) (file *File, err error)

In the above signature, if the file has been opened successfully, then the Open function will return the file handler, and the error will be nil. If there is an error while opening the file, a non-nil error will be returned.

If a function or method returns an error, then by convention, it has to be the last value returned from the function. Hence the Open function returns err as to the last value.

The idiomatic way of handling errors in Go is to compare the returned error to nil. A nil value indicates that no error has occurred, and a non-nil value indicates the presence of an error.

In our case, we check whether the error is not nil. If it is not nil, we simply print the error and return it from the main function.

Error type representation

Let’s dig a little deeper and see how the built-in error type is defined. error is an interface type with the following definition,

type error interface {  
    Error() string
}

The above interface contains only one single method with signature Error() string. Any type which implements this interface can be used as an error. This method describes the error.

When printing the error, fmt.Println function calls the Error() string method internally to get the description of the error.

Different ways to extract more information from the error

Now that we know the error is an interface type let’s see how we can extract more information about an error.

In the above example, we have just printed the description of the error. What if we wanted the actual path of the file, which caused the error. One possible way to get this is to parse the error string. This was the output of our program,

open /data.txt: No such file or directory  

We can parse this error message and get the file path “/data.txt” of the file that caused the error, but this is a dirty way of doing it. The error description can change at any time in newer versions of Go, and our code will break.

Is there a better way to get the file name ?? The answer is yes, it can be done, and the Go standard library uses different ways to provide more information about errors. Let’s look at them one by one.

1. Asserting the underlying struct type and getting more information from the struct fields

If you read the documentation of the Open function carefully, you can see that it returns an error of type *PathError.PathError is a struct type, and its implementation in the standard library is as follows,

type PathError struct {  
    Op   string
    Path string
    Err  error
}

func (e *PathError) Error() string { 
    return e.Op + " " + e.Path + ": " + e.Err.Error() 
}

From the above code, you can understand that *PathError implements the error interface by declaring the Error() string method. This method concatenates the operation, path, and actual error and returns it. Thus we got the error message,

open /data.txt: No such file or directory  

The Path field of the PathError struct contains the path of the file which caused the error. Let’s modify the program we wrote above and print the path.

package main

import (  
    "fmt"
    "os"
)

func main() {  
    f, err := os.Open("data.txt")
    if err != nil {
        if pErr, ok := err.(*os.PathError); ok {
            fmt.Println("Failed to open file at path", pErr.Path)
            return
        }
        fmt.Println("Generic error", err)
        return
    }
    fmt.Println(f.Name(), "opened successfully")
}

Output

Failed to open file at path data.txt

Run in playground

In the above program, we first check whether the error is not nil in line no. 10, and then we use type assertion in line no. 11 to get the underlying value of the error interface.

If you have not heard about type assertion before, I recommend reading https://waytoeasylearn.com/learn/interfaces-in-go/. Then we print the path using pErr.Path in line no. 12.

In case the underlying error is not of type *os.PathError, the control will reach line no. 15, and a generic error message will be printed.

Great ?. We have successfully used type assertion to get the file path from the error.

2. Asserting the underlying struct type and getting more information using methods

The second way to get more information from the error is to assert the underlying type and get more information by calling methods on the struct type.

Let’s understand this better using an example. The DNSError struct type in the standard library is defined as follows,

type DNSError struct {  
    ...
}

func (e *DNSError) Error() string {  
    ...
}
func (e *DNSError) Timeout() bool {  
    ... 
}
func (e *DNSError) Temporary() bool {  
    ... 
}

The DNSError struct has two methods, Timeout() bool and Temporary() bool, which return a boolean value that indicates whether the error is because of a timeout or is it a temporary one.

Let’s write a program that asserts the *DNSError type and calls these methods to determine whether the error is temporary or due to timeout.

package main

import (  
    "fmt"
    "net"
)

func main() {  
    addr, err := net.LookupHost("waytoeasylearn123.com")
    if err != nil {
        if dnsErr, ok := err.(*net.DNSError); ok {
            if dnsErr.Timeout() {
                fmt.Println("Operation timed out")
                return
            }
            if dnsErr.Temporary() {
                fmt.Println("Temporary error")
                return
            }
            fmt.Println("Generic DNS error", err)
            return
        }
        fmt.Println("Generic error", err)
        return
    }
    fmt.Println(addr)
}

Run in playground

Please note that DNS lookups do not work in the playground. Please run this program on your local machine.

In the program above, in line no. 9, we are trying to get the IP address of an invalid domain name waytoeasylearn123.com. In line no. 11, we get the underlying value of the error by asserting it to type *net.DNSError.

Then we check whether the error is due to timeout or is temporary in line nos. 12 and 16, respectively.

In our case, the error is neither temporary nor due to timeout, and hence the program will print,

Generic DNS error lookup waytoeasylearn123.com: no such host  

If the error was temporary or due to a timeout, then the corresponding if statement would have been executed, and we can handle it appropriately.

3. Direct comparison

The third way to get more details about an error is the direct comparison with a variable of type error. Let’s understand this using an example.

The Glob function of the filepath package is used to return all files’ names that match a pattern. This function returns an error ErrBadPattern when the pattern is malformed.

ErrBadPattern is defined in the filepath package as a global variable.

var ErrBadPattern = errors.New("syntax error in pattern")

errors.New() is used to create a new error. We will discuss this in detail in the next tutorial. ErrBadPattern is returned by the Glob function when the pattern is malformed.

Let’s write a small program to check for this error.

package main

import (  
    "fmt"
    "path/filepath"
)

func main() {  
    files, err := filepath.Glob("[")
    if err != nil {
        if err == filepath.ErrBadPattern {
            fmt.Println("Bad pattern error:", err)
            return
        }
        fmt.Println("Generic error:", err)
        return
    }
    fmt.Println("matched files", files)
}

Output

Bad pattern error: syntax error in pattern  

Run in playground

In the program above, we search for files of the pattern [ which is a malformed pattern. We check whether the error is not nil.

Now to get more information about the error, we directly comparing it to filepath.ErrBadPattern in line. no 11. If the condition is satisfied, then the error is due to a malformed pattern.

Error Handling in Go
Scroll to top