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 testtargets in lesson 13.