> series: learn go from 0 to 100 | article 9 of 19
Every dev landing in Go goes through the same two phases: first “am I seriously writing if err != nil every three lines?” and then, a few months later, “ok, now I get why”. This Go error handling article exists to shorten the trip between the two.
Errors are values
Go has no exceptions flying overhead: Go error handling treats an error as a regular value implementing a one-method interface (Error handling and Go):
// error is just a stdlib interface:
// type error interface { Error() string }
f, err := os.Open("config.json")
if err != nil {
// handle and bail early: happy path stays left
return fmt.Errorf("opening config: %w", err)
}
defer f.Close()
The philosophical consequence is huge: the error is part of the function’s signature, visible at every call site, impossible to ignore by accident. In Java throws gets dodged, in Python the except gets forgotten, in JS the rejected promise vanishes into limbo. In Go the error looks you in the eye from the return value (FAQ, exceptions).
The resulting visual pattern is called happy path on the left: errors get handled and cut short with early return, and the happy flow runs straight down with no indentation.
Wrapping errors: %w, errors.Is and errors.As
Since Go 1.13, errors chain together (Working with Errors in Go 1.13 and the errors package):
// %w wraps: the original error stays inside.
err := fmt.Errorf("querying customer: %w", sql.ErrNoRows)
// errors.Is asks for a specific error in the chain.
if errors.Is(err, sql.ErrNoRows) {
fmt.Println("customer not found, not a big deal")
}
// errors.As looks for a TYPE in the chain and extracts it.
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("path failed:", pathErr.Path)
}
The %w verb in fmt.Errorf adds context without destroying the original. Each layer of your app adds its crumb, and errors.Is can still ask about the root cause at the bottom of the chain. Compare with Java stack traces. Here you choose the context, layer by layer, and it reads like a sentence.

Sentinel errors and errors with data
Two ways to create your own errors, each with its use case:
// Sentinel error: exported variable to compare against.
var ErrInsufficientFunds = errors.New("insufficient funds")
// Error with data: a struct implementing Error().
type ValidationError struct {
Field string
Value any
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("invalid field %q: %v", e.Field, e.Value)
}
The sentinel (exported ErrSomething, by convention prefixed Err) is for known conditions the caller will want to distinguish via errors.Is.
The struct is for errors carrying payload, fished out with errors.As. And remember article 7‘s gotcha: always return error as the type, never a concrete pointer.
panic and recover: the emergency exit
Yes, panic exists. No, it’s not your try/catch (Effective Go, panic):
func process() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
panic("something impossible just happened")
}
The community convention, and Effective Go‘s, is clear: panic is reserved for the unrecoverable (internal corruption, logical impossibilities) and recover for keeping one goroutine from taking down the whole process, as the stdlib HTTP server does. Expected errors (missing file, invalid input, network down) ALWAYS travel as values. If you use panic for control flow, somewhere in the world a gopher sheds a tear.
Weekly challenge: put Go error handling to work
- Write ParseAge(s string) (int, error) wrapping strconv.Atoi’s error with %w and rejecting ages outside 0-130 with your own error.
- Create an ErrNotFound sentinel in a lookup function and distinguish it at the call site with errors.Is.
- Define ValidationError with field and value, throw it wrapped in two layers of context and extract it with errors.As.
- Bonus: trigger a panic with an out-of-range index, catch it with recover in a defer and turn the scare into a regular error.
Go error handling: level complete.
Next article: pointers. What they are, when to use them, and why in Go they’re not the horror show they were in C.
> exit