Lesson 03 of 9 · Part 1 — The language
Errors done right
Handle errors the Go way: errors are values you return and check, wrap them with context, inspect them with errors.Is and errors.As, clean up with defer, and keep panic for programmer mistakes.
Errors are values
Functions that can fail return an error as their last result. The caller checks it right away:
package main
import (
"fmt"
"os"
"strings"
)
func readToken(path string) (string, error) {
b, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("read token %s: %w", path, err)
}
return strings.TrimSpace(string(b)), nil
}
func main() {
tok, err := readToken("/var/run/secrets/kubernetes.io/serviceaccount/token")
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
fmt.Println("token length:", len(tok))
}
Compared with Python exceptions, failure paths are visible in the code. Compared with Bash, you can't forget set -e: an unused err is easy to spot, and linters flag ignored errors.
A Go function hands you two things: the parcel and a delivery note. Before opening the parcel, you read the note. If the note says "damaged", you don't open it; you pass the note along with a line of your own: "damaged on the way to the warehouse".
Wrap with context
Each layer adds what it was doing, so the final message reads like a path:
error: sync cluster prod-eu: list nodes: Get "https://10.0.0.1:6443/api/v1/nodes": dial tcp 10.0.0.1:6443: i/o timeout
Use %w to wrap (keeps the chain), and lower-case messages without trailing punctuation so they join cleanly.
Inspect: errors.Is and errors.As
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
var ErrNoNodes = errors.New("no nodes found")
type QuotaError struct {
Resource string
Limit int
}
func (e *QuotaError) Error() string { return fmt.Sprintf("quota exceeded for %s (limit %d)", e.Resource, e.Limit) }
func scale(n int) error {
if n == 0 {
return fmt.Errorf("scale pool: %w", ErrNoNodes)
}
if n > 10 {
return fmt.Errorf("scale pool: %w", &QuotaError{Resource: "cpus", Limit: 40})
}
return nil
}
func main() {
for _, n := range []int{0, 20, 3} {
err := scale(n)
var qe *QuotaError
switch {
case err == nil:
fmt.Println(n, "ok")
case errors.Is(err, ErrNoNodes):
fmt.Println(n, "nothing to do")
case errors.As(err, &qe):
fmt.Println(n, "ask for more", qe.Resource)
}
}
if _, err := os.Stat("/nope"); errors.Is(err, fs.ErrNotExist) {
fmt.Println("file missing")
}
}
errors.Ismatches a specific value anywhere in the chain (sentinels such asfs.ErrNotExist,context.DeadlineExceeded).errors.Asfinds an error of a type and gives you its fields.- Kubernetes code uses helpers in the same spirit:
apierrors.IsNotFound(err),apierrors.IsConflict(err)(lesson 07).
defer for clean-up
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // runs when the function returns, on every path
Deferred calls run in reverse order. Common uses: closing files and response bodies, unlocking mutexes, cancelling contexts.
panic is for bugs
Return errors for anything that can happen in normal operation: missing files, timeouts, API errors, bad user input. panic is for states that should be impossible (an index you've already checked, a nil that can't be nil). Long-running services and controllers recover from panics at the top of each request or reconcile so one bad object doesn't kill the process; controller-runtime does this for you.
Try it: an error you can act on
- Write
loadConfig(path string) (Config, error)that reads and parses a YAML or JSON file, wrapping each failure with context. - Call it with a missing file, a file with bad syntax and a good file; print the full error each time.
- Add a sentinel
ErrMissingClusterreturned when a required field is empty, and handle it specially witherrors.Is. - Add a custom error type with a field and handle it with
errors.As. - Install
errcheck(or golangci-lint) and find an ignored error you added on purpose.
Going deeper: errors at boundaries
- Log an error once, where you handle it (usually at the top), not at every layer that returns it.
- Use
errors.Jointo return several errors at once (validation of many fields). - At process boundaries, map errors to exit codes (CLI) or HTTP status codes (service) deliberately.
Recap
- Errors are returned values; check them immediately.
- Wrap with
fmt.Errorf("doing x: %w", err)so messages read as a path. - Inspect with errors.Is (values) and errors.As (types).
- defer for clean-up; panic only for bugs.
This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.