Error Handling in Go
Go treats errors as values, not exceptions. Functions that can fail return error as their last return value — you see every failure explicitly.
💬 Go philosophy: “Errors are just values.” — Rob Pike. No hidden control flow, no stack unwinding, no
try/catch.
1. The error Interface
type error interface {
Error() string
}Any type with an Error() string method satisfies this interface — that’s all it takes.
2. Creating Errors
import (
"errors"
"fmt"
)
err := errors.New("something went wrong") // simple, static message
err := fmt.Errorf("user %d not found", userID) // with formatting
err := fmt.Errorf("db query: %w", originalErr) // wrapping (Go 1.13+)| Method | Use when |
|---|---|
errors.New | Simple static message |
fmt.Errorf | Dynamic message with values |
fmt.Errorf("%w", err) | Wrapping — caller can inspect the cause |
| Custom struct | Caller needs to extract structured fields |
3. Checking Errors
result, err := divide(10, 0)
if err != nil {
log.Printf("divide failed: %v", err)
return err
}
// safe to use result hereGolden rule: Check
err != nilimmediately after every call that returns one. Never use the result whenerr != nil.
4. Error Wrapping & Unwrapping (Go 1.13+) ⭐
// Wrap — adds context without losing the original
func readConfig(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("readConfig %s: %w", path, err)
}
_ = data
return nil
}// errors.Is — checks if a target exists anywhere in the chain
if errors.Is(err, os.ErrNotExist) {
fmt.Println("file not found")
}
// errors.As — extracts a typed error from the chain
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("bad path:", pathErr.Path)
}| Function | Purpose |
|---|---|
fmt.Errorf("%w", err) | Wraps — preserves the error chain |
errors.Is(err, target) | Checks if target appears anywhere in the chain |
errors.As(err, &target) | Extracts a typed error from the chain |
errors.Unwrap(err) | Returns the directly wrapped error (one level) |
5. Custom Error Types
Use when callers need to extract fields from the error, not just the message.
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
}
func validateAge(age int) error {
if age < 0 {
return &ValidationError{Field: "age", Message: "must be non-negative"}
}
return nil
}
// Caller extracts the structured info
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("invalid field:", ve.Field)
}6. Sentinel Errors
Package-level error variables that callers compare with errors.Is.
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
)
func getUser(id int) (User, error) {
if id == 0 {
return User{}, ErrNotFound
}
return User{}, nil
}
// Caller
if errors.Is(err, ErrNotFound) {
http.Error(w, "not found", http.StatusNotFound)
}Convention: Prefix sentinel errors with
Err(e.g.ErrNotFound,ErrTimeout).
7. panic & recover
Use panic only for unrecoverable programmer errors — never for normal business logic.
// panic — for invariant violations or startup failures
func mustParseURL(raw string) *url.URL {
u, err := url.Parse(raw)
if err != nil {
panic(fmt.Sprintf("invalid URL %q: %v", raw, err))
}
return u
}
// recover — catch panics (e.g. HTTP middleware, goroutines)
func safeMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
log.Printf("panic: %v", rec)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next(w, r)
}
}8. Error vs panic
error | panic | |
|---|---|---|
| When | Expected, recoverable condition | Unrecoverable programmer bug |
| Control flow | Normal return path | Unwinds the stack immediately |
| Handled with | if err != nil | defer + recover() |
| Performance | Lightweight | Heavy — captures stack trace |
| Examples | File not found, bad input | Nil deref, index out of bounds, invariant violation |
9. Production Patterns
Always wrap with context — never return bare err
// ❌ Caller has no idea where this came from
return err
// ✅ Each layer adds context
return fmt.Errorf("processOrder %d: %w", orderID, err)Handle OR return — never both
// ❌ Error gets logged multiple times up the chain
log.Printf("error: %v", err)
return err
// ✅ Pick one: handle it here, or pass it up
return fmt.Errorf("createUser: %w", err)10. Common Mistakes ⚠️
| Mistake | Problem | Fix |
|---|---|---|
_ = someFunc() | silently discards error | always check or explicitly log |
Return bare err | caller loses context | wrap with fmt.Errorf("op: %w", err) |
| Log + return err | same error logged multiple times | handle OR return, not both |
err == ErrFoo | breaks with wrapped errors | use errors.Is(err, ErrFoo) |
panic for business errors | crashes the server | return error instead |
| No wrapping in layers | stack of meaningless messages | add context at every layer |
11. Interview Cheat Sheet
Q: How does Go handle errors?
As values. Functions return
(result, error). Callers checkif err != nilexplicitly — no exceptions, no hidden control flow. Every failure is visible at the call site.
Q: What is error wrapping?
fmt.Errorf("context: %w", err)creates a new error that chains the original. The chain can be inspected witherrors.Is(value match) orerrors.As(type extraction).
Q: Difference between errors.Is and errors.As?
errors.Is(err, target)checks iftargetappears anywhere in the error chain (equality).errors.As(err, &target)checks if any error in the chain matches the type oftargetand assigns it.
Q: What is a sentinel error?
A package-level
var ErrXxx = errors.New(...)that callers compare usingerrors.Is. Avoids fragile string matching and survives wrapping.
Q: When should you use panic?
Only for unrecoverable programmer errors: startup failures, invariant violations, or situations where continuing would cause worse bugs. Never for user input errors or I/O failures.
Q: Difference between errors.New and fmt.Errorf?
errors.Newcreates a simple static error.fmt.Errorfsupports format verbs and — critically —%wfor wrapping. For dynamic messages always usefmt.Errorf.