DEV Community

Ankit malik
Ankit malik

Posted on

Custom errors in Go

Introduction

Golang or Go has a robust error-handling mechanism that's integral to the language's design. While Go provides built-in error types, there are situations where you might need more control and context in your error handling.
This is where creating custom errors comes into play. Custom errors can give you more informative error messages and can be used to categorize different types of errors in your application.

In this article, we'll explore how to create and use custom errors in Golang effectively.

Understanding Go's Built-in Error Handling

In Go, the error type is a built-in interface that looks like this:

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

Any type that implements the Error() method with a string return type satisfies this interface and can be considered an error. This is simple but powerful because it allows you to create custom error types by simply implementing this method.

Basic Error Handling in Go

Here's a quick example of basic error handling in Go:

package main

import (
    "errors"
    "fmt"
)

func main() {
    err := doSomething()
    if err != nil {
        fmt.Println("Error:", err)
    }
}

func doSomething() error {
    return errors.New("something went wrong")
}
Enter fullscreen mode Exit fullscreen mode

This example uses the errors.New function to create a basic error. While this is useful for simple cases, it lacks the ability to provide more context or to distinguish between different types of errors.

Why Create Custom Errors?

Custom errors are essential when you need more descriptive error messages or when you want to handle different types of errors differently. For example, you might want to return a specific error type when a file is not found, and another type when a file is corrupted. Custom errors can also carry additional data, making debugging easier and providing more detailed information to the caller.

Creating Custom Errors in Go

To create a custom error in Go, you define a new type that implements the Error() method. Let's walk through an example.

Example 1: A Simple Custom Error

Here’s how you can create a simple custom error:

package main

import (
    "fmt"
)

type MyError struct {
    Code    int
    Message string
}

func (e *MyError) Error() string {
    return fmt.Sprintf("Code %d: %s", e.Code, e.Message)
}

func main() {
    err := doSomething()
    if err != nil {
        fmt.Println("Error:", err)
    }
}

func doSomething() error {
    return &MyError{
        Code:    404,
        Message: "Resource not found",
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, MyError is a custom error type that includes a code and a message. The Error() method formats these into a string, making it easy to print or log the error.

Example 2: Checking errors with errors.Is function

Sometimes, it's useful to compare errors directly. This is where sentinel errors come in. A sentinel error is a predefined, exported variable that represents a specific error.

package main

import (
    "errors"
    "fmt"
)

var ErrNotFound = errors.New("resource not found")

func main() {
    err := doSomething()
    if errors.Is(err, ErrNotFound) {
        fmt.Println("Error:", err)
    }
}

func doSomething() error {
    return ErrNotFound
}
Enter fullscreen mode Exit fullscreen mode

While this approach is straightforward, combining sentinel values with custom error types can be even more powerful, allowing for both error comparison and rich error data.

Example 3: Wrapping Errors for More Context

Go 1.13 introduced the fmt.Errorf function with %w verb, which allows you to wrap errors, adding more context while preserving the original error.

package main

import (
    "errors"
    "fmt"
)

var ErrNotFound = errors.New("resource not found")

func main() {
    err := doSomething()
    if err != nil {
        fmt.Println("Error:", err)
        if errors.Is(err, ErrNotFound) {
            fmt.Println("The resource was not found.")
        }
    }
}

func doSomething() error {
    err := fetchResource()
    if err != nil {
        return fmt.Errorf("failed to do something: %w", err)
    }
    return nil
}

func fetchResource() error {
    return ErrNotFound
}
Enter fullscreen mode Exit fullscreen mode

This allows you to check for specific errors and also maintain a stack of errors that gives more context about what went wrong.

Example 4: Enriching Errors with the Unwrap() Method

Go also provides a way to extract the wrapped error using the Unwrap() method. By implementing this method in your custom error types, you can allow further unwrapping of errors.

package main

import (
    "errors"
    "fmt"
)

type MyError struct {
    Code    int
    Message string
    Err     error
}

func (e *MyError) Error() string {
    return fmt.Sprintf("Code %d: %s", e.Code, e.Message)
}

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

func main() {
    err := doSomething()
    if err != nil {
        fmt.Println("Error:", err)
        if errors.Is(err, ErrNotFound) {
            fmt.Println("The resource was not found.")
        }
    }
}

var ErrNotFound = errors.New("resource not found")

func doSomething() error {
    err := fetchResource()
    if err != nil {
        return &MyError{
            Code:    500,
            Message: "Something went wrong",
            Err:     err,
        }
    }
    return nil
}

func fetchResource() error {
    return ErrNotFound
}
Enter fullscreen mode Exit fullscreen mode

Here, MyError has an Unwrap() method that returns the wrapped error. This allows for deeper inspection and handling of the underlying error.

Example 5: Checking Errors with errors.As function

Go also provides a way to extract the error with errors.As method. By implementing this method in your custom error types, you can check error type and also fetch the custom error values.

package main

import (
    "errors"
    "fmt"
)

type MyError struct {
    Code    int
    Message string
    Err     error
}

func (e *MyError) Error() string {
    return fmt.Sprintf("Code %d: %s: %v", e.Code, e.Message, e.Err)
}

func main() {
    err := doSomething()
    var mErr *MyError
    // another way to initialize custom error
    // mErr := &MyError{}
    if errors.As(err, &mErr) {
        fmt.Println("Error:", mErr)
    }
}

// doSomething attempts to fetch a resource and returns an error if it fails.
// If the error is ErrNotFound, it is wrapped in a MyError with a code of 500.
func doSomething() error {
    err := fetchResource()
    if err != nil {
        return &MyError{
            Code:    500,
            Message: "Something went wrong",
            Err:     err,
        }
    }
    return nil
}

var ErrNotFound = errors.New("resource not found")

func fetchResource() error {
    return ErrNotFound
}

Enter fullscreen mode Exit fullscreen mode

Best Practices for Custom Errors in Go

  1. Use Custom Errors Sparingly: Custom errors are powerful but can add complexity. Use them when they provide significant benefits, like better error categorization or additional context.

  2. Leverage Wrapping and Unwrapping: Wrapping errors with additional context and unwrapping them later is a best practice that enhances error debugging.

  3. Document Your Error Types: Ensure that any custom errors are well-documented so that their purpose and usage are clear.

  4. Prefer Error Values for Comparison: When you need to compare errors, consider using predefined error values (sentinel errors) for consistency and clarity.

Conclusion

Custom errors in Go provide a flexible and powerful way to manage errors in your applications. By creating your own error types, wrapping errors for additional context, and unwrapping them for deeper inspection, you can build robust and maintainable error-handling mechanisms. This not only helps in debugging but also improves the overall quality of your Go code.
Choose your strategy with the errors and use the consistent errors in the whole project.

Top comments (0)