Errors as Values

Updated

July 30, 2026

Overview

Go treats errors as values, not exceptions. Functions return errors alongside results, making error handling explicit and visible.

The error Interface

type error interface {
    Error() string
}

Returning Errors

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

result, err := divide(10, 0)
if err != nil {
    log.Fatal(err)
}

Creating Errors

import "errors"

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

// Formatted error
err := fmt.Errorf("failed to open %s: %v", filename, originalErr)

Error Handling Patterns

Check Immediately

result, err := doSomething()
if err != nil {
    return err
}
// Use result

Early Return

func process() error {
    data, err := fetch()
    if err != nil {
        return err
    }

    result, err := transform(data)
    if err != nil {
        return err
    }

    return save(result)
}

Add Context

data, err := readFile(path)
if err != nil {
    return fmt.Errorf("loading config: %w", err)
}

Custom Error Types

type ValidationError struct {
    Field   string
    Message string
}

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

func validate(u User) error {
    if u.Name == "" {
        return &ValidationError{Field: "name", Message: "required"}
    }
    return nil
}

Sentinel Errors

var ErrNotFound = errors.New("not found")
var ErrPermission = errors.New("permission denied")

func find(id int) (*User, error) {
    if !exists(id) {
        return nil, ErrNotFound
    }
    // ...
}

// Check
if err == ErrNotFound {
    // Handle not found
}

Summary

Pattern Usage
errors.New() Simple error
fmt.Errorf() Formatted error
Custom type Structured error data
Sentinel Known error values

Worked example

Multi-step pipeline that returns, wraps, and classifies errors without panicking.

Save as main.go. Then:

go mod init example
go run .
package main

import (
    "errors"
    "fmt"
)

var ErrEmpty = errors.New("empty input")

type ParseError struct {
    Token string
    Why   string
}

func (e *ParseError) Error() string {
    return fmt.Sprintf("parse %q: %s", e.Token, e.Why)
}

func parseIntToken(s string) (int, error) {
    if s == "" {
        return 0, ErrEmpty
    }
    n := 0
    for _, r := range s {
        if r < '0' || r > '9' {
            return 0, &ParseError{Token: s, Why: "not digits"}
        }
        n = n*10 + int(r-'0')
    }
    return n, nil
}

func sumTokens(tokens []string) (int, error) {
    total := 0
    for i, t := range tokens {
        n, err := parseIntToken(t)
        if err != nil {
            return 0, fmt.Errorf("sumTokens[%d]: %w", i, err)
        }
        total += n
    }
    return total, nil
}

func main() {
    got, err := sumTokens([]string{"10", "20", "12"})
    fmt.Println("ok sum:", got, err)

    _, err = sumTokens([]string{"10", "", "3"})
    fmt.Println("empty err:", err)
    fmt.Println("Is ErrEmpty:", errors.Is(err, ErrEmpty))

    _, err = sumTokens([]string{"10", "x9"})
    var pe *ParseError
    if errors.As(err, &pe) {
        fmt.Println("As ParseError token:", pe.Token, "why:", pe.Why)
    }
}

Expected output:

ok sum: 42 <nil>
empty err: sumTokens[1]: empty input
Is ErrEmpty: true
As ParseError token: x9 why: not digits

More examples

Sentinel comparison vs string equality—only the value works reliably after wrapping.

package main

import (
    "errors"
    "fmt"
)

var ErrDenied = errors.New("denied")

func authorize(role string) error {
    if role != "admin" {
        return fmt.Errorf("authorize(%s): %w", role, ErrDenied)
    }
    return nil
}

func main() {
    err := authorize("guest")
    // Correct: compare the sentinel through the chain.
    fmt.Println("Is denied:", errors.Is(err, ErrDenied))
    // Fragile: string form changes when you add context.
    fmt.Println("string equal ErrDenied:", err.Error() == ErrDenied.Error())
    fmt.Println("err text:", err)
}

Expected output:

Is denied: true
string equal ErrDenied: false
err text: authorize(guest): denied

Runnable example

Save as main.go. Then:

go mod init example
go run .
package main

import (
    "errors"
    "fmt"
)

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

type ValidationError struct {
    Field   string
    Message string
}

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

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

func findUser(id int) (string, error) {
    if id <= 0 {
        return "", ErrNotFound
    }
    return fmt.Sprintf("user-%d", id), nil
}

func validateName(name string) error {
    if name == "" {
        return &ValidationError{Field: "name", Message: "required"}
    }
    return nil
}

func main() {
    q, err := divide(10, 2)
    fmt.Println("divide ok:", q, err)
    _, err = divide(1, 0)
    fmt.Println("divide err:", err)

    name, err := findUser(7)
    fmt.Println("find:", name, err)
    _, err = findUser(0)
    if errors.Is(err, ErrNotFound) {
        fmt.Println("sentinel: not found")
    }

    if err := validateName(""); err != nil {
        fmt.Println("validation:", err)
    }
    if err := validateName("Ada"); err != nil {
        fmt.Println("unexpected:", err)
    } else {
        fmt.Println("validation: ok")
    }
}

Expected output:

divide ok: 5 <nil>
divide err: division by zero
find: user-7 <nil>
sentinel: not found
validation: name: required
validation: ok

What to notice: Errors are ordinary values you return and inspect. Sentinel errors (ErrNotFound) and custom types (ValidationError) both implement the same error interface.

Try next: Use fmt.Errorf("findUser(%d): %w", id, ErrNotFound) and compare string form vs errors.Is; add a second field to ValidationError for a code.