Go

Error Handling

Go has no exceptions at all — a deliberate design decision, not a missing feature. Every function that can fail returns an explicit error value alongside its normal result, using exactly the multiple-return-value mechanism from earlier in this domain. This chapter is that convention in full, and why Go's designers chose it over C++'s exceptions.

Quick reference
What it is
Errors as ordinary values, returned explicitly from functions and checked immediately by the caller — no throw/catch mechanism exists in the language.
Mental model
Where C++ says "stop everything and unwind until someone catches this," Go says "hand the error back like any other value, and let the caller decide right here, right now."
Key numbers
error is a one-method interface (Error() string) — the simplest possible interface in the language, and one of its most used.
Reach for it when
Every function that can fail, essentially — checking if err != nil immediately after almost every call is the normal shape of idiomatic Go code.

The error interface

type error interface {
    Error() string
}

That's the entire built-in definition — any type with an Error() string method satisfies error, using the same implicit-implementation rule from Interfaces. Creating one is usually this simple:

err := errors.New("file not found")
fmt.Println(err.Error())   // file not found
fmt.Println(err)             // file not found — fmt knows to call Error() automatically

The if err != nil convention

func readConfig(path string) (string, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return "", err   // pass the error straight up to OUR caller
    }
    return string(data), nil   // nil error means success
}

func main() {
    config, err := readConfig("settings.txt")
    if err != nil {
        fmt.Println("failed to read config:", err)
        return
    }
    fmt.Println(config)
}

nil is Go's zero value for an interface — an error that's nil means "nothing went wrong." This is genuinely close to C's own return-code convention in spirit, but Go's version is checked by strong, near-universal convention rather than being merely legal to ignore.

Wrapping errors for context

fmt.Errorf with %w wraps an underlying error with additional context, while keeping the original error inspectable:

func loadUser(id int) (*User, error) {
    data, err := readConfig("user.txt")
    if err != nil {
        return nil, fmt.Errorf("loading user %d: %w", id, err)   // adds context, keeps the original
    }
    // ...
}

// If it fails: "loading user 42: open user.txt: no such file or directory"
// errors.Is() and errors.Unwrap() can still recover the original error from the wrapped one

This solves a real problem plain string concatenation can't: the caller several layers up still has access to the original error, not just a string that mentions it, letting code check for a specific underlying failure even through several layers of added context.

panic and recover

Go does have panic, but it's reserved for genuinely unrecoverable situations — a programming bug, not an expected failure condition:

func mustPositive(n int) int {
    if n < 0 {
        panic("mustPositive: negative input")   // this should never happen if the code is correct
    }
    return n
}

The rule of thumb Go's own standard library follows: return an error for anything a caller could reasonably expect and handle (a missing file, a bad network response); reserve panic for violated invariants that indicate an actual bug. This is a narrower, more deliberate use than C++ exceptions, which are commonly used for a much wider range of error conditions.

Real-world applications

Gotchas

Field notes