Go Custom Errors

Custom errors carry more information than a plain string message. By creating a struct that implements the error interface, programs can attach structured data to an error — like which field failed, what the invalid value was, or what operation caused the problem. This makes error handling precise and machine-readable.

What Makes a Type an Error?

Any type that has an Error() string method satisfies the error interface.

type error interface {
    Error() string
}

To create a custom error, define a struct and give it an Error() method.

Basic Custom Error

package main

import "fmt"

type AppError struct {
    Code    int
    Message string
}

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

func processOrder(id int) error {
    if id <= 0 {
        return &AppError{Code: 400, Message: "order ID must be positive"}
    }
    return nil
}

func main() {
    err := processOrder(-1)
    if err != nil {
        fmt.Println(err) // Error 400: order ID must be positive
    }
}

Custom Error Structure Diagram

type AppError struct
┌──────────────────────────────┐
│  Code    │  400              │
│  Message │  "invalid input"  │
└──────────────────────────────┘
           │
           └── Error() string method makes it an error interface

err.Error() → "Error 400: invalid input"

Sentinel Errors – Predefined Error Values

Sentinel errors are package-level error variables. They represent well-known error conditions that callers can check with errors.Is.

package main

import (
    "errors"
    "fmt"
)

var (
    ErrNotFound   = errors.New("item not found")
    ErrPermission = errors.New("permission denied")
    ErrTimeout    = errors.New("operation timed out")
)

func fetchItem(id int) error {
    if id == 0 {
        return ErrNotFound
    }
    return nil
}

func main() {
    err := fetchItem(0)

    switch {
    case errors.Is(err, ErrNotFound):
        fmt.Println("Handle not found:", err)
    case errors.Is(err, ErrPermission):
        fmt.Println("Handle permission error:", err)
    default:
        fmt.Println("Unknown error:", err)
    }
}

Rich Custom Error with Context

package main

import (
    "errors"
    "fmt"
)

type ValidationError struct {
    Field   string
    Value   interface{}
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed on field '%s' (value: %v): %s",
        e.Field, e.Value, e.Message)
}

func validateAge(age int) error {
    if age < 0 {
        return &ValidationError{
            Field:   "age",
            Value:   age,
            Message: "age cannot be negative",
        }
    }
    if age > 150 {
        return &ValidationError{
            Field:   "age",
            Value:   age,
            Message: "age is unrealistically high",
        }
    }
    return nil
}

func main() {
    err := validateAge(-5)
    if err != nil {
        fmt.Println(err)

        var ve *ValidationError
        if errors.As(err, &ve) {
            fmt.Println("Field:", ve.Field)
            fmt.Println("Value:", ve.Value)
        }
    }
}

Output:

validation failed on field 'age' (value: -5): age cannot be negative
Field: age
Value: -5

Wrapping Custom Errors

Custom errors can wrap other errors to preserve the original cause.

package main

import (
    "errors"
    "fmt"
)

type DBError struct {
    Operation string
    Err       error
}

func (e *DBError) Error() string {
    return fmt.Sprintf("database error during %s: %v", e.Operation, e.Err)
}

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

var ErrConnection = errors.New("connection refused")

func queryDB() error {
    return &DBError{
        Operation: "SELECT",
        Err:       ErrConnection,
    }
}

func main() {
    err := queryDB()
    fmt.Println(err)

    if errors.Is(err, ErrConnection) {
        fmt.Println("Root cause: connection refused") // works via Unwrap
    }
}

Key Points

  • Any type with an Error() string method satisfies the error interface
  • Custom error structs carry structured data beyond a plain message
  • Sentinel errors are package-level variables for well-known error conditions
  • Implement Unwrap() error on a custom error to support errors.Is and errors.As on wrapped chains
  • Use errors.As to extract a custom error type from an error chain

Leave a Comment