iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Error Handling

Go has no exceptions. Functions return (value, error) and you handle errors at the call site. With errors.Is, errors.As, and wrapping via %w, the model becomes powerful for real apps.

Idiomatic error handling

EXAMPLE
package main

import (
    "errors"
    "fmt"
    "io"
    "net/http"
)

// 1) Sentinel errors — values you can compare with errors.Is
var (
    ErrNotFound      = errors.New("not found")
    ErrUnauthorized  = errors.New("unauthorized")
)

// 2) Wrapped errors — preserve cause, add context
func loadUser(id string) (User, error) {
    body, err := fetch("/users/" + id)
    if err != nil {
        return User{}, fmt.Errorf("loadUser(%s): %w", id, err)
    }
    var u User
    if err := json.Unmarshal(body, &u); err != nil {
        return User{}, fmt.Errorf("loadUser(%s) decode: %w", id, err)
    }
    return u, nil
}

// 3) Inspect at the boundary with errors.Is / errors.As
u, err := loadUser(id)
if err != nil {
    if errors.Is(err, ErrNotFound) {
        http.Error(w, "unknown user", 404); return
    }
    if errors.Is(err, io.EOF) {
        // remote closed — retry
    }
    var dnsErr *net.DNSError
    if errors.As(err, &dnsErr) {
        log.Warn("dns: " + dnsErr.Name)
    }
    log.Error(err)
    http.Error(w, "internal", 500); return
}

// 4) Custom error types — carry data
type ValidationError struct {
    Field, Reason string
}
func (v *ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", v.Field, v.Reason)
}

func parseAge(s string) (int, error) {
    n, err := strconv.Atoi(s)
    if err != nil { return 0, &ValidationError{"age", "not a number"} }
    if n < 0     { return 0, &ValidationError{"age", "must be >= 0"} }
    return n, nil
}

var v *ValidationError
if errors.As(err, &v) {
    return Problem{Field: v.Field, Detail: v.Reason}
}

// 5) Don't ignore errors
if _, err := os.Open("x"); err != nil {
    return err           // good
}
_, _ = io.Copy(dst, src) // bad — silently drops everything

// 6) panic / recover — only at process boundaries (servers, goroutine entry)
func safeHandler(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if r := recover(); r != nil {
                log.Error("panic: ", r, debug.Stack())
                http.Error(w, "internal", 500)
            }
        }()
        h(w, r)
    }
}

Why it matters

fmt.Errorf(\"...: %w\", err) wraps the cause, preserving errors.Is/As while adding a breadcrumb. Wrap at each layer; inspect once at the boundary.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
f, err := os.Open("data.txt")
if err != nil {
    return fmt.Errorf("open: %w", err)
}
defer f.Close()
Try it Yourself »

Exercise

Idiomatic error check.

if err nil { return err }

Test yourself

Q1. Idiomatic Go returns errors as…
Q2. Wrap an error with extra context using…
Q3. Unwrap an error with…

Discussion

Loading…