The Must Pattern & No-GC Handling

Advanced Error Patterns: The “Must” & The “Odin” Way

Go’s error handling (if err != nil) is explicit, but sometimes you need different strategies for initialization vs. runtime, or you want to adopt patterns from systems languages like Odin/Zig.

1. The “Must” Pattern

Rule: Return errors to callers; Panic only on programmer error or unrecoverable startup failure.

The “Must” pattern is a convention for functions that wrap a standard error-returning function but panic instead of returning the error.

When to use it?

  • Global/Package Initialization: var t = template.Must(template.New(...))
  • Startup Configuration: If your compiled regex is invalid, the app is broken. Don’t start.

Implementation

func Must[T any](obj T, err error) T {
    if err != nil {
        panic(err)
    }
    return obj
}

// Usage
var db = Must(sql.Open("postgres", "..."))
var regex = Must(regexp.Compile("^[a-z]+$"))

With Generics (Go 1.18+), you can write a universal Must wrapper one time and use it everywhere.


2. Linear Error Handling (The Odin/Zig Influence)

Languages like Odin (a data-oriented C alternative) handle errors by treating them as distinct return values, much like Go, but often with syntactic sugar (or_return) or strict definitions.

While Go doesn’t have try or ? operators, we can adopt the “Guard Clause” mentality.

The Happy Path must remain left-aligned

Bad (Nested):

func process() error {
    err := step1()
    if err == nil {
        err = step2()
        if err == nil {
            return step3()
        }
    }
    return err
}

Good (Guard Clauses / “Odin Style”):

func process() error {
    if err := step1(); err != nil {
        return fmt.Errorf("step1: %w", err)
    }

    // The "Happy Path" stays at indentation 0
    if err := step2(); err != nil {
        return fmt.Errorf("step2: %w", err)
    }

    return step3()
}

No-GC Thinking: Errors as Resource Cleanup Triggers

In non-GC languages, an error often implies manual resource cleanup (defer in Swift/Zig). Go automates memory, but not resources (Files, Sockets, DB Connections).

The defer Trap in Loops:

for _, file := range files {
    f, err := os.Open(file)
    if err != nil { return err }
    defer f.Close() // DANGEROUS: Closes only at end of function, not loop!
    // FD exhaustion possible.
}

The Fix (Anonymous Function):

for _, file := range files {
    err := func() error {
        f, err := os.Open(file)
        if err != nil { return err }
        defer f.Close() // Closes at end of this anonymous func
        return process(f)
    }()
    if err != nil { return err }
}

3. Errors in 2026: “Join” and Structure

Since Go 1.20+, errors.Join allows returning multiple errors at once (e.g., from parallel validation).

func validate(u User) error {
    var errs error
    if u.Name == "" {
        errs = errors.Join(errs, errors.New("missing name"))
    }
    if u.Age < 0 {
        errs = errors.Join(errs, errors.New("invalid age"))
    }
    return errs // Returns nil if no errors joined
}

This is cleaner than older multierror libraries.

Summary

  1. Must: Use for hard startup dependencies. Crash early.
  2. Left-Align: Keep your happy path on the left edge.
  3. Defer scope: Remember defer is function-scoped, not block-scoped.