Error Handling Basics
Go treats errors as values implementing the error interface: Error() string.
Introduction
Go treats errors as values implementing the error interface: Error() string. There are no exceptions for control flow — panics are reserved for truly unrecoverable programmer errors. Production services build error chains with fmt.Errorf and %w, inspect them with errors.Is and errors.As, and define sentinel errors with errors.New.
Understanding when to wrap vs return raw errors, how to create custom error types with additional fields, and defer+recover for graceful shutdown separates junior from mid-level Go developers in interviews.
This lesson covers the error handling patterns used in Kubernetes, etcd, and every major Go open-source project — explicit, verbose, but predictable and debuggable.
The story
When a Kubernetes controller fails to patch a CustomResource, it wraps the API error with context: fmt.Errorf("reconcile deployment %s: %w", name, err). Operators use errors.Is to detect context.DeadlineExceeded and retry, or errors.As to extract structured API status details — the same patterns in client-go that every platform engineer reads during incident response.
Explicit error handling is verbose but predictable: no hidden stack unwinding across goroutine boundaries in a payment service processing millions of transactions.
Understanding the topic
Key concepts
- error interface: type with Error() string method.
- errors.New and fmt.Errorf create errors; %w wraps for errors.Is/As.
- Sentinel errors: var ErrNotFound = errors.New("not found").
- Custom errors: type MyError struct { Code int; Msg string }.
- errors.Is(err, target) checks chain; errors.As(err, &target) extracts type.
- panic/recover — recover only useful inside defer; don't use for normal flow.
flowchart TDCall[Function Call] --> Err{error != nil?}Err -->|yes| Handle[Handle / Wrap]Err -->|no| Success[Use Result]Handle --> Propagate[Return Up]
Step-by-step explanation
- Function returns error as last value; caller checks if err != nil.
- Wrap: fmt.Errorf("fetch user %s: %w", id, err) preserves chain.
- Compare: errors.Is(err, ErrNotFound) walks unwrap chain.
- Extract: var e *MyError; errors.As(err, &e) gets typed error.
- panic stops normal execution; recover in defer catches it.
- log.Fatal calls os.Exit — use only in main, not libraries.
Practical code example
Sentinel errors, wrapping, and errors.Is/As inspection:
package mainimport ("errors""fmt")var ErrNotFound = errors.New("not found")type ValidationError struct {Field stringReason string}func (e *ValidationError) Error() string {return fmt.Sprintf("validation: %s — %s", e.Field, e.Reason)}func findUser(id string) error {if id == "" {return &ValidationError{Field: "id", Reason: "required"}}if id == "missing" {return fmt.Errorf("db lookup %s: %w", id, ErrNotFound)}return nil}func main() {err := findUser("missing")if errors.Is(err, ErrNotFound) {fmt.Println("handled: user not found")}var ve *ValidationErrorif errors.As(findUser(""), &ve) {fmt.Printf("validation failed on %s\n", ve.Field)}}
Line-by-line code explanation
if err != nilis the mandatory check after almost every fallible operation.fmt.Errorf("context: %w", err)wraps an error while preserving the chain forerrors.Is.errors.Is(err, context.DeadlineExceeded)checks whether any wrapped error matches a sentinel.errors.As(err, &apiErr)extracts a typed error (e.g.,*APIStatusError) from the chain.var ErrNotFound = errors.New("not found")defines a package-level sentinel for domain errors.return fmt.Errorf("fetch zone %s: %w", id, err)adds human context without losing the root cause.errors.Join(err1, err2)(Go 1.20+) combines multiple errors from parallel operations.log.Printf("warning: %v", err)logs the outermost message; use%+vwith pkg/errors for stacks.
Key takeaway: %w enables errors.Is/As. Custom error types carry structured data for API responses. Never panic in libraries.
Real-world use
Where you'll use this in production
- HTTP handlers mapping ErrNotFound to 404, validation to 400.
- Database layer translating sql.ErrNoRows to domain errors.
- Retry logic checking errors.Is(err, context.DeadlineExceeded).
- Structured logging with error chains in distributed tracing.
Best practices
- Add context when wrapping: operation + identifier + %w.
- Export sentinel errors for API consumers to check with errors.Is.
- Return error, never panic, from library functions.
- Use fmt.Errorf with %w, not %v, when caller needs Is/As.
- Handle errors once — wrap upward or log at top, not both repeatedly.
- Document exported error variables in godoc.
Common mistakes
- Comparing err == ErrNotFound when err is wrapped — use errors.Is.
- Using panic for validation errors in HTTP handlers.
- Swallowing errors with _ — silent failures in production.
- Wrapping with %v instead of %w — breaks error chain.
- log.Fatal in reusable packages — prevents caller cleanup.
Advanced interview questions
Q1BeginnerGo error interface?
Q2Beginnerpanic vs error return?
Q3IntermediatePurpose of %w in fmt.Errorf?
Q4Intermediateerrors.Is vs errors.As?
Q5AdvancedDesign error handling for REST API with domain errors.
Summary
Errors are values; check if err != nil after every fallible call. Wrap with fmt.Errorf and %w; inspect with errors.Is/As. Sentinel and custom typed errors enable structured handling. panic/recover only for truly exceptional cases, not control flow. Next lesson: arrays — fixed-size collections.