Drills

Drills — errors as values

The real content of this lesson isn't files — it's errors as ordinary values you create, return, pass around and inspect. Five drills build that muscle, no disk needed. Write each, run it, compare.

1 (easy) — parsePositive. Return (int, error). Reuse Atoi's error when the text isn't a number, and make your own error when the number breaks your rule.

parsePositive("380") → 380, nil      parsePositive("-5") → 0, "-5 is not positive"
func parsePositive(s string) (int, error) {
    n, err := strconv.Atoi(s)
    if err != nil {
        return 0, err
    }
    if n <= 0 {
        return 0, fmt.Errorf("%d is not positive", n)
    }
    return n, nil
}

fmt.Errorf builds an error value from a message — that's all an error is underneath.

2 (easy) — status. An error is a value you can look at. Return "ok" when there's no error, otherwise the error's text.

status(nil) → "ok"      status(parseError) → "-5 is not positive"
func status(err error) string {
    if err == nil {
        return "ok"
    }
    return err.Error()
}

The err == nil check is the whole discipline of Go error handling: nil means "nothing went wrong", anything else means it did.

3 (medium) — firstError. Given several errors, return the first real one (or nil if all are clean). Errors are values, so you can loop over them like anything else.

firstError([]error{nil, nil, parseError}) → parseError      firstError([]error{nil, nil}) → nil
func firstError(errs []error) error {
    for _, e := range errs {
        if e != nil {
            return e
        }
    }
    return nil
}

4 (medium) — loadCount, propagate and wrap. Real code rarely handles an error where it happens — it hands it up to the caller, adding context. That's fmt.Errorf with %w ("wrap"):

loadCount("42") → 42, nil      loadCount("-5") → 0, "loadCount: -5 is not positive"
func loadCount(s string) (int, error) {
    n, err := parsePositive(s)
    if err != nil {
        return 0, fmt.Errorf("loadCount: %w", err)
    }
    return n, nil
}

%w keeps the original error inside the new one — the message gains a prefix, and the original is still recoverable (drill 5). This if err != nil { return ..., err } shape is the single most common thing you'll write in real Go.

5 (harder) — a sentinel error and errors.Is. Sometimes the caller needs to react to a specific error — the way this lesson's loadItems treats "file not found" as a normal first run. You define a named sentinel error and check for it with errors.Is, which sees through %w wrapping:

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

func loadCount(s string) (int, error) {
    if s == "" {
        return 0, ErrEmpty
    }
    n, err := parsePositive(s)
    if err != nil {
        return 0, fmt.Errorf("loadCount: %w", err)
    }
    return n, nil
}
_, err := loadCount("")
fmt.Println(errors.Is(err, ErrEmpty)) // true — react to THIS error specifically

That's exactly errors.Is(err, fs.ErrNotExist) from step 1, now with an error you defined yourself.

Tip. Notice not one of these opened a file — yet they're the entire skill that makes file code safe. Files just supply errors; deciding what each one means is your job, everywhere. These pure functions are, once more, ideal go test targets in lesson 13.