Go Error Handling

Go handles errors as regular values, not exceptions. A function that can fail returns an error as its last return value. The caller checks that error immediately after the function call. This approach makes error handling explicit, readable, and impossible to accidentally ignore.

The error Type

The built-in error type is an interface with one method: Error() string. A function returns nil when everything goes well, and a non-nil error when something fails.

package main

import (
    "errors"
    "fmt"
)

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("cannot divide by zero")
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 2)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Result:", result) // Result: 5
    }

    result2, err2 := divide(10, 0)
    if err2 != nil {
        fmt.Println("Error:", err2) // Error: cannot divide by zero
    }
}

Error Handling Flow

result, err := divide(10, 0)
                    │
                    ▼
              function runs
                    │
              b == 0? Yes
                    │
              returns (0, error)
                    │
          ┌─────────┴──────────┐
        err != nil           err == nil
          │                    │
     handle error          use result

Creating Errors

errors.New

import "errors"

err := errors.New("something went wrong")

fmt.Errorf – Formatted Error Messages

import "fmt"

age := -5
err := fmt.Errorf("invalid age: %d — must be 0 or greater", age)

Wrapping Errors with fmt.Errorf and %w

The %w verb wraps an existing error inside a new one, preserving the original for inspection later.

package main

import (
    "errors"
    "fmt"
)

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

func findUser(id int) error {
    return fmt.Errorf("findUser(%d): %w", id, ErrNotFound)
}

func main() {
    err := findUser(42)
    fmt.Println(err) // findUser(42): record not found

    if errors.Is(err, ErrNotFound) {
        fmt.Println("The original error is ErrNotFound") // printed
    }
}

errors.Is – Checking Error Identity

errors.Is checks whether an error (or any error it wraps) matches a specific target error.

if errors.Is(err, ErrNotFound) {
    // handle not-found specifically
}

errors.As – Checking Error Type

errors.As checks whether an error (or any wrapped error) is a specific type, and extracts it for inspection.

package main

import (
    "errors"
    "fmt"
)

type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", e.Field, e.Message)
}

func validate(name string) error {
    if name == "" {
        return &ValidationError{Field: "name", Message: "cannot be empty"}
    }
    return nil
}

func main() {
    err := validate("")

    var ve *ValidationError
    if errors.As(err, &ve) {
        fmt.Println("Field:", ve.Field)   // Field: name
        fmt.Println("Issue:", ve.Message) // Issue: cannot be empty
    }
}

The Standard Error Pattern

SituationWhat to Do
Simple error messageerrors.New("message")
Error with context/valuesfmt.Errorf("context: %w", err)
Check error identityerrors.Is(err, targetErr)
Check error typeerrors.As(err, &target)
No error occurredReturn nil

Key Points

  • Errors in Go are values — return them like any other value
  • Always check if err != nil immediately after a function that returns an error
  • nil means success; a non-nil error means failure
  • Use fmt.Errorf with %w to wrap errors and preserve the original
  • Use errors.Is and errors.As to inspect wrapped error chains

Leave a Comment