Sling Academy
Home/Golang/Exploring the `error` Interface: Building Custom Error Types in Go

Exploring the `error` Interface: Building Custom Error Types in Go

Last updated: November 26, 2024

Error handling is a crucial part of building robust and reliable applications. Go (Golang) has a built-in error interface that serves as the foundation for handling errors. To build your own custom error types, you need to understand how Go interfaces work and how errors should be constructed following Go’s idioms.

Understanding the Go `error` Interface

The Go error interface is quite simple. It consists of just one method:

type error interface {
    Error() string
}

The Error() method returns the error message as a string. This simplicity allows a great deal of flexibility; you can create various error types that implement this interface.

Basic Example: Implementing a Custom Error

Let’s start with a basic example. Imagine a scenario where a function needs to return a specific type of error if a numeric condition isn’t met:

package main

import (
    "fmt"
)

// Define a custom error type.
type SimpleError struct {
    ErrMessage string
}

// Implement the Error method for SimpleError.
func (e *SimpleError) Error() string {
    return e.ErrMessage
}
}

// A function demonstrating how to return a SimpleError
func FailWhenNegative(num int) error {
    if num < 0 {
        return &SimpleError{ErrMessage: "Number cannot be negative"}
    }
    return nil
}

func main() {
    // Attempt an operation that could fail.
    if err := FailWhenNegative(-1); err != nil {
        fmt.Println(err)
    }
}

In this basic example, we defined SimpleError with an error message field, and implemented the Error() method to satisfy the error interface. The FailWhenNegative function checks a condition and returns an error if the condition isn’t met.

Intermediate Example: Adding More Functionality to Your Custom Error

Now, let’s make the custom error more informative by adding additional context such as codes or underlying errors:

package main

import (
    "fmt"
)

// CustomError holds an error message, code, and optionally another error.
type CustomError struct {
    ErrMessage string
    Code       int
    Err        error
}

// Implement the Error method for CustomError.
func (e *CustomError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("Error %d: %s - Root cause: %v", e.Code, e.ErrMessage, e.Err)
    }
    return fmt.Sprintf("Error %d: %s", e.Code, e.ErrMessage)
}

// Function with more complex error.
func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, &CustomError{ErrMessage: "Division by zero", Code: 400, Err: fmt.Errorf("b was %d", b)}
    }
    return a / b, nil
}

func main() {
    // An operation that can fail
    if result, err := Divide(4, 0); err != nil {
        fmt.Println(err)
    } else {
        fmt.Println("The result is", result)
    }
}

In this example, CustomError provides an error code and can optionally hold another error instance to trace the root of an issue more explicitly. This is helpful when handling errors in nested operations.

Advanced Example: Wrapping and Unwrapping Errors

Go 1.13 introduced error wrapping and unwrapping. It is particularly useful for creating nested errors with straightforward stack tracing:

package main

import (
    "errors"
    "fmt"
)

// WrapAndDivide to wrap low-level errors
func WrapAndDivide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

func HandleDivide(a, b int) (int, error) {
    result, err := WrapAndDivide(a, b)
    if err != nil {
        // Wrap the error with additional context
        return 0, fmt.Errorf("HandleDivide failed: %w", err)
    }
    return result, nil
}

func main() {
    if result, err := HandleDivide(10, 0); err != nil {
        // Unwrap the error
        if errors.Is(err, errors.New("division by zero")) {
            fmt.Println("Cannot perform division due to zero denominator.")
        }
        fmt.Println(err)
    } else {
        fmt.Println("The result is", result)
    }
}

This example shows error wrapping with the %w format verb, which allows you to add context to an error. The errors.Is() function permits checking if an error matches a specific one within a stack of wrapped errors.

Conclusion

Understanding and leveraging customized error types in Go can significantly improve error handling in your applications. By developing error types that implement the error interface, you tailor the error reporting to meet your application’s specific needs. This ensures that the operations that use these custom errors can handle issues effectively at every abstraction level of your program stack.

Next Article: Implementing Observer Patterns Using Interfaces in Go

Previous Article: How to Mock Interfaces for Unit Testing in Go

Series: Structs and Interfaces in Go

Golang

Related Articles

You May Also Like

  • How to remove HTML tags in a string in Go
  • How to remove special characters in a string in Go
  • How to remove consecutive whitespace in a string in Go
  • How to count words and characters in a string in Go
  • Relative imports in Go: Tutorial & Examples
  • How to run Python code with Go
  • How to generate slug from title in Go
  • How to create an XML sitemap in Go
  • How to redirect in Go (301, 302, etc)
  • Using Go with MongoDB: CRUD example
  • Auto deploy Go apps with CI/ CD and GitHub Actions
  • Fixing Go error: method redeclared with different receiver type
  • Fixing Go error: copy argument must have slice type
  • Fixing Go error: attempted to use nil slice
  • Fixing Go error: assignment to constant variable
  • Fixing Go error: cannot compare X (type Y) with Z (type W)
  • Fixing Go error: method has pointer receiver, not called with pointer
  • Fixing Go error: assignment mismatch: X variables but Y values
  • Fixing Go error: array index must be non-negative integer constant