Files can be missing, input can be invalid, and network requests can fail. These are ordinary outcomes that a caller needs to handle. In Go, a function usually reports such failure by returning an error alongside its result. The caller checks the error before using the result, then decides whether to explain, retry, fall back, or return the failure upward.
error is an interface with an Error() string method. A nil error conventionally means success. A non-nil error describes failure. An error message helps a human understand what happened; program logic should usually inspect the error's identity or type instead of comparing message strings. The error-handling tutorial introduces the return-and-check pattern.
Run a layered validation example
Create a fresh module, save this as main.go, and run go run .:
package main
import (
"errors"
"fmt"
"strconv"
)
var ErrOutOfRange = errors.New("count outside allowed range")
type InputError struct {
Field string
Value string
Err error
}
func (e *InputError) Error() string {
return fmt.Sprintf("%s %q: %v", e.Field, e.Value, e.Err)
}
func (e *InputError) Unwrap() error {
return e.Err
}
func parseCount(raw string) (int, error) {
count, err := strconv.Atoi(raw)
if err != nil {
return 0, &InputError{Field: "count", Value: raw, Err: err}
}
if count < 1 || count > 10 {
return 0, &InputError{Field: "count", Value: raw, Err: ErrOutOfRange}
}
return count, nil
}
func makePlan(raw string) (string, error) {
count, err := parseCount(raw)
if err != nil {
return "", fmt.Errorf("make plan: %w", err)
}
return fmt.Sprintf("plan for %d items", count), nil
}
func main() {
for _, raw := range []string{"3", "0", "many"} {
plan, err := makePlan(raw)
if err == nil {
fmt.Println(plan)
continue
}
if errors.Is(err, ErrOutOfRange) {
fmt.Println("choose a count from 1 to 10")
}
var inputErr *InputError
if errors.As(err, &inputErr) {
fmt.Println("invalid field:", inputErr.Field)
}
fmt.Println(err)
}
}
The valid input prints plan for 3 items. Input "0" prints a range instruction, identifies count, and prints a message beginning make plan: count "0":. Input "many" identifies the same field and reports the parsing failure. The exact nested parser message belongs to strconv; your error handling does not depend on its wording.
Add context without losing the cause
The package-level ErrOutOfRange is a sentinel error, a reusable value representing one recognizable condition. InputError adds structured information. Its Unwrap method exposes the cause. makePlan then uses %w to add operation context while preserving that cause.
errors.Is(err, ErrOutOfRange) checks whether that condition appears through the wrapping structure. errors.As(err, &inputErr) finds a matching error type and gives the caller access to its fields. The extra address operator is intentional: As needs somewhere to store the matching *InputError. The errors package documents both operations.
Use %v when you only want text; use %w when callers should be able to inspect the wrapped error. Wrapping exposes part of your error contract, so choose causes callers can use meaningfully. Do not turn every internal implementation detail into a promise to consumers. The Go error-wrapping article discusses this API-design tradeoff.
Decide where the error is handled
Low-level code should usually return useful context, while an application boundary decides how to present the failure. Logging the same error at every level produces repeated messages without additional information. A command-line program may print a detailed error; an HTTP API should send a deliberate client-safe response and log internal details separately.
Check errors before deferring cleanup on an acquired resource. If an operation succeeds but its later flush or close can fail, decide how that failure reaches the caller too. A successful function call does not guarantee all subsequent resource work will succeed.
Pitfalls: ignored errors and panic
Assigning an error to _ discards information. Sometimes ignoring an error is justified, but it should follow from the operation's contract, not from wanting fewer lines. Likewise, returning zero and nil after a failed parse silently turns bad input into plausible data.
panic interrupts ordinary control flow and runs deferred calls while unwinding that goroutine's stack. It fits violated internal assumptions better than routine validation failures. recover can intercept a panic when called in an appropriate deferred function in the same goroutine; it does not replace an explicit error contract. Consult Defer, Panic, and Recover for the mechanics.
Exercise: recognize malformed numbers
Extend main to print a separate instruction when "many" fails numeric parsing. Keep the range response for "0". Do not search for a substring in the error message.
Solution guidance
Declare var numberErr *strconv.NumError and use errors.As(err, &numberErr). The wrapping path still leads to that parser error. Print an instruction such as enter a whole number. Verify that "0" only takes the range branch and "many" only takes the numeric branch, while both still identify the invalid field. Test the boundaries "1" and "10" as successful cases.
Previous: Interfaces. Next: Generics.

