Skip to Content
Go Realm v1 is released 🎉
TopicsError Handling

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+)
MethodUse when
errors.NewSimple static message
fmt.ErrorfDynamic message with values
fmt.Errorf("%w", err)Wrapping — caller can inspect the cause
Custom structCaller 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 here

Golden rule: Check err != nil immediately after every call that returns one. Never use the result when err != 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) }
FunctionPurpose
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

errorpanic
WhenExpected, recoverable conditionUnrecoverable programmer bug
Control flowNormal return pathUnwinds the stack immediately
Handled withif err != nildefer + recover()
PerformanceLightweightHeavy — captures stack trace
ExamplesFile not found, bad inputNil 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 ⚠️

MistakeProblemFix
_ = someFunc()silently discards erroralways check or explicitly log
Return bare errcaller loses contextwrap with fmt.Errorf("op: %w", err)
Log + return errsame error logged multiple timeshandle OR return, not both
err == ErrFoobreaks with wrapped errorsuse errors.Is(err, ErrFoo)
panic for business errorscrashes the serverreturn error instead
No wrapping in layersstack of meaningless messagesadd context at every layer

11. Interview Cheat Sheet

Q: How does Go handle errors?

As values. Functions return (result, error). Callers check if err != nil explicitly — 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 with errors.Is (value match) or errors.As (type extraction).

Q: Difference between errors.Is and errors.As?

errors.Is(err, target) checks if target appears anywhere in the error chain (equality). errors.As(err, &target) checks if any error in the chain matches the type of target and assigns it.

Q: What is a sentinel error?

A package-level var ErrXxx = errors.New(...) that callers compare using errors.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.New creates a simple static error. fmt.Errorf supports format verbs and — critically — %w for wrapping. For dynamic messages always use fmt.Errorf.