Understanding Custom Error Types in Go

August 18, 2024

In Go, error handling is a fundamental aspect of writing robust and maintainable code. The language provides a simple error interface for representing errors, but sometimes you need more than just a basic error message. In these cases, creating custom error types can be very useful. This blog post will walk you through creating a custom error type in Go, demonstrating its benefits, and showing how to use errors.As and errors.Is effectively.

The error package

The errors package provides utilities for creating and manipulating errors in Go. Its primary function is to generate error values that contain a specified text message.

// Package errors implements functions to manipulate errors.
//
// The New function creates errors whose only content is a text message.

package errors

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
	return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
	s string
}

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

Custom Error Types

Custom error types in Go can provide richer error information. They can include additional fields and methods to provide more context about the error. Here’s an example of how to define and use a custom error type.

Let’s start by defining a custom error type that includes an exit code and an underlying error. We’ll use the following interface and struct:

type NotFound interface {
    error
    ExitCode() int
}

type notFound struct {
    Err  error
    Code int
}

func (nf *notFound) Error() string {
    return fmt.Sprintf("error caused due to %v", nf.Err)
}

func (nf *notFound) ExitCode() int {
    return nf.Code
}

func (nf *notFound) Unwrap() error {
    return nf.Err
}

func main() {
    err := &notFound{
        Err:  fmt.Errorf("resource not found"),
        Code: 404,
    }
    fmt.Println(err.Unwrap())
}

Using errors.Is

The errors.Is function examines the error tree to determine if a specific error is present in the chain. It traverses the tree in a depth-first, pre-order manner, checking if any error in the chain matches the target error.

    err := fmt.Errorf("file operation failed: %w", baseErr)
    
   if errors.Is(err, fs.ErrExist) {
        fmt.Println("File already exists")
    }

Using errors.As

The errors.As function inspects the error tree to see if any error in the chain can be assigned to a specific error type. It performs a type assertion, allowing you to extract and work with the underlying error if it matches the specified type.

var perr *fs.PathError
if errors.As(err, &perr) {
    fmt.Println(perr.Path)
}

is preferable to

if perr, ok := err.(*fs.PathError); ok {
	fmt.Println(perr.Path)
}

By understanding and using these error handling techniques, you can write more robust and informative error handling code in Go, making it easier to diagnose and respond to issues in your applications.