Pro Tips — Error Handling
Go's Error Handling Philosophy
package main
import (
"errors"
"fmt"
)
// Philosophy 1: Errors are values
// Don't treat errors specially — handle them like regular values
type Result[T any] struct {
Value T
Err error
}
func NewResult[T any](val T, err error) Result[T] {
return Result[T]{Value: val, Err: err}
}
func (r Result[T]) IsOK() bool { return r.Err == nil }
func (r Result[T]) Unwrap() (T, error) { return r.Value, r.Err }
// Philosophy 2: Handle failures — never ignore them
func badStyle() {
// Bad: ignoring errors
// result, _ := someOperation()
// If an error occurs, you won't know about it
}
// Philosophy 3: Enrich error context
var ErrNotFound = errors.New("not found")
// Bad: no idea where this originated
func badGetUser(id int) (string, error) {
if id <= 0 {
return "", ErrNotFound
}
return "user", nil
}
// Good: includes context
func goodGetUser(id int) (string, error) {
if id <= 0 {
return "", fmt.Errorf("GetUser(id=%d): %w", id, ErrNotFound)
}
return "user", nil
}
func main() {
// Treating errors as values
results := []Result[int]{
NewResult(42, nil),
NewResult(0, fmt.Errorf("calculation failed: %w", ErrNotFound)),
}
for _, r := range results {
val, err := r.Unwrap()
if err != nil {
fmt.Println("error:", err)
if errors.Is(err, ErrNotFound) {
fmt.Println("→ not found")
}
} else {
fmt.Println("value:", val)
}
}
// Context comparison
_, err1 := badGetUser(-1)
_, err2 := goodGetUser(-1)
fmt.Printf("\nbad error: %v\ngood error: %v\n", err1, err2)
}
Reducing if err != nil Patterns
package main
import (
"fmt"
"strconv"
)
// Pattern 1: errWriter — handle errors collectively like io.Writer
type errWriter struct {
data []byte
err error
}
func (ew *errWriter) writeString(s string) {
if ew.err != nil {
return // skip if already errored
}
ew.data = append(ew.data, s...)
}
func (ew *errWriter) writeInt(n int) {
if ew.err != nil {
return
}
ew.data = append(ew.data, strconv.Itoa(n)...)
}
func (ew *errWriter) validate(cond bool, msg string) {
if ew.err != nil {
return
}
if !cond {
ew.err = fmt.Errorf("validation: %s", msg)
}
}
func buildPayload(name string, age int) ([]byte, error) {
ew := &errWriter{}
ew.validate(name != "", "name is required")
ew.validate(age >= 0, "age must be non-negative")
ew.validate(age <= 150, "age is too large")
ew.writeString(`{"name":"`)
ew.writeString(name)
ew.writeString(`","age":`)
ew.writeInt(age)
ew.writeString(`}`)
return ew.data, ew.err
}
// Pattern 2: functional chaining
type Pipeline[T any] struct {
value T
err error
}
func NewPipeline[T any](val T) *Pipeline[T] {
return &Pipeline[T]{value: val}
}
func (p *Pipeline[T]) Then(fn func(T) (T, error)) *Pipeline[T] {
if p.err != nil {
return p
}
p.value, p.err = fn(p.value)
return p
}
func (p *Pipeline[T]) Result() (T, error) {
return p.value, p.err
}
// Pattern 3: consolidate error handling with defer
func processWithDefer(id int) (result string, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("processWithDefer(id=%d): %w", id, err)
}
}()
if id <= 0 {
return "", fmt.Errorf("invalid id")
}
return fmt.Sprintf("processed-%d", id), nil
}
func main() {
// errWriter pattern
fmt.Println("=== errWriter pattern ===")
data, err := buildPayload("Alice", 30)
if err == nil {
fmt.Println("payload:", string(data))
}
_, err = buildPayload("", -1)
if err != nil {
fmt.Println("validation failed:", err)
}
// Pipeline pattern
fmt.Println("\n=== Pipeline pattern ===")
result, err := NewPipeline(" hello world ").
Then(func(s string) (string, error) {
if s == "" {
return "", fmt.Errorf("empty string")
}
return "trimmed: " + s, nil
}).
Then(func(s string) (string, error) {
return "[" + s + "]", nil
}).
Result()
if err != nil {
fmt.Println("pipeline error:", err)
} else {
fmt.Println("pipeline result:", result)
}
// defer error wrapping
fmt.Println("\n=== defer error wrapping ===")
for _, id := range []int{1, -1} {
res, err := processWithDefer(id)
if err != nil {
fmt.Println("error:", err)
} else {
fmt.Println("result:", res)
}
}
}
Structured Logging with slog
package main
import (
"errors"
"fmt"
"log/slog"
"os"
)
// Error code type
type AppError struct {
Code int
Message string
Err error
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error { return e.Err }
// Pattern for integrating slog with errors
func logError(logger *slog.Logger, err error, msg string, args ...any) {
if err == nil {
return
}
// Extract code from AppError
var appErr *AppError
if errors.As(err, &appErr) {
logger.Error(msg,
append(args,
"error", err,
"code", appErr.Code,
"cause", appErr.Err,
)...,
)
return
}
logger.Error(msg, append(args, "error", err)...)
}
var ErrNotFound = errors.New("not found")
func fetchUser(id int) (string, error) {
if id <= 0 {
return "", &AppError{Code: 400, Message: "invalid user id"}
}
if id > 100 {
return "", &AppError{Code: 404, Message: "user not found", Err: ErrNotFound}
}
return fmt.Sprintf("User-%d", id), nil
}
func main() {
// JSON format structured logger
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
// Text format (for development)
textLogger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
ids := []int{1, 0, 999}
for _, id := range ids {
user, err := fetchUser(id)
if err != nil {
logError(logger, err, "user fetch failed", "user_id", id)
logError(textLogger, err, "user fetch failed", "user_id", id)
} else {
logger.Info("user fetch success", "user_id", id, "user", user)
}
}
}
Error Handling Anti-Patterns and Correct Patterns
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
// ====== Anti-patterns ======
// Anti-pattern 1: Ignoring errors
func antiPattern1() {
val := 0
_ = val // completely ignoring an error
// Correct: handle the error or propagate it upward
}
// Anti-pattern 2: Using panic for normal error control flow
func antiPattern2(id int) string {
if id <= 0 {
panic("invalid id") // Bad! Normal errors should return error
}
return fmt.Sprintf("item-%d", id)
}
// Anti-pattern 3: Comparing errors by string
func antiPattern3(err error) bool {
return err.Error() == "not found" // Bad! String comparison is fragile
}
// Anti-pattern 4: Propagating errors without context
func antiPattern4() error {
err := ErrNotFound
return err // Bad! No idea where this originated
}
// ====== Correct patterns ======
// Correct pattern 1: Error propagation with context
func goodPattern1(id int) (string, error) {
if id <= 0 {
return "", fmt.Errorf("goodPattern1: invalid id %d: %w", id, ErrNotFound)
}
return fmt.Sprintf("item-%d", id), nil
}
// Correct pattern 2: Compare with errors.Is
func goodPattern2(err error) bool {
return errors.Is(err, ErrNotFound) // Good!
}
// Correct pattern 3: Use errors.As for type checking
type ValidationError struct{ Field, Msg string }
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation[%s]: %s", e.Field, e.Msg)
}
func goodPattern3(err error) {
var ve *ValidationError
if errors.As(err, &ve) { // Good! Type-safe extraction
fmt.Printf("field %q validation failed: %s\n", ve.Field, ve.Msg)
}
}
// Correct pattern 4: Early return
func goodPattern4(a, b, c int) (int, error) {
// Immediate return at each step — reduces nesting
if a <= 0 {
return 0, fmt.Errorf("a must be positive: %d", a)
}
if b <= 0 {
return 0, fmt.Errorf("b must be positive: %d", b)
}
if c <= 0 {
return 0, fmt.Errorf("c must be positive: %d", c)
}
return a + b + c, nil
}
// Correct pattern 5: Group error handling (Go 1.20+)
func goodPattern5(ids []int) error {
var errs []error
for _, id := range ids {
if id <= 0 {
errs = append(errs, fmt.Errorf("invalid id: %d", id))
}
}
return errors.Join(errs...)
}
func main() {
// Pattern comparison
fmt.Println("=== error propagation ===")
_, err := goodPattern1(-1)
fmt.Println("error:", err)
fmt.Println("Is ErrNotFound:", goodPattern2(err))
fmt.Println("\n=== type-safe error check ===")
ve := &ValidationError{Field: "email", Msg: "invalid format"}
wrapped := fmt.Errorf("process: %w", ve)
goodPattern3(wrapped)
fmt.Println("\n=== early return ===")
result, err := goodPattern4(1, 0, 3)
if err != nil {
fmt.Println("error:", err)
} else {
fmt.Println("result:", result)
}
fmt.Println("\n=== error grouping ===")
joinErr := goodPattern5([]int{1, -1, 0, 2, -3})
if joinErr != nil {
fmt.Println("compound error:", joinErr)
}
fmt.Println("\n=== summary ===")
fmt.Println("1. Never ignore errors (minimize _ usage)")
fmt.Println("2. Compare with errors.Is, extract type with errors.As")
fmt.Println("3. Wrap with %w to preserve context")
fmt.Println("4. Never use panic for normal errors")
fmt.Println("5. Use early return to minimize nesting")
}
Core Rules
- Errors are values— don't treat them specially, handle like regular values
- Never ignore errors— discarding with
_is the start of bugs- Add context with
%w— record where and why it failederrors.Is/errors.As— type-safe comparison instead of string comparison- panic is a last resort— never use for normal error situations
- Early return— reduce nesting and improve readability
- Structured logging with slog— log error codes and context together