Home/Go/Errors, defer, and resource ownership

Errors, defer, and resource ownership

Error identity, cleanup ordering, panic unwinding and ownership boundaries

An error is an ordinary interface value returned alongside useful results. Check it before using a result whose contract depends on success. A sentinel error represents a stable category; a custom error type can carry structured fields. Wrap with fmt.Errorf and %w when adding context while preserving classification. errors.Is follows wrapping to test a category, and errors.As extracts a matching structured error. Comparing text is brittle because messages are for people and can change.

go
base := errors.New("unavailable")
wrapped := fmt.Errorf("read configuration: %w", base)
fmt.Println(errors.Is(wrapped, base)) // true

Defer binds cleanup to scope

A deferred call runs when the surrounding function returns or unwinds through a panic. Deferred calls execute in last-in, first-out order. Its function and argument values are evaluated at the defer statement, while a deferred closure reads captured variables when invoked. Register cleanup immediately after successful acquisition so later early returns cannot skip it. Do not defer a close inside a large long-running loop if it must release each resource before the next iteration; extract a per-iteration function or close explicitly.

Resource ownership must be stated. A function may borrow a reader and leave closing to its caller, or consume an owned resource and close it exactly once. It should not silently mix the two. When both work and cleanup fail, choose an error policy: preserve the primary work failure, return the cleanup failure only if work succeeded, or combine errors when the caller benefits from both. The exercise uses primary-error precedence and tests it explicitly.

Panic is not ordinary validation

Use errors for expected input, I/O and capacity failures. Panic indicates a violated assumption or exceptional failure; recover is generally placed at a deliberate boundary that can restore a meaningful state. A recover in another goroutine cannot catch this goroutine's panic. Deferred cleanup is still necessary even when a panic propagates. This lesson's implementation must not swallow a panic from its source.

A returned byte slice may alias a reusable buffer owned by the source. Closing or reading that source again can mutate the buffer, so detach the bytes before exposing them to a caller when the API promises ownership. Copying only the slice descriptor is insufficient. On failure, partial bytes are discarded here; streaming APIs sometimes intentionally return data plus an error and require a different contract.

These rules apply to files, response bodies, transactions and temporary resources. Successful-path tests alone miss leaked resources and overwritten primary errors; explicitly test every exit path, including panic unwinding.

Put it into practice

Implement the exercise in a full-screen workspace, then run its tests.

Open practice
Next lessonGenerics and constraints