template/docs/result-slides.md
2026-05-04 19:15:11 +00:00

6.8 KiB
Raw Blame History

theme highlightTheme margin html marp
gaia monokai 0 true true

result

Happy-Path Error Handling in Go

Focus on success. Handle failure at the boundary.


The Problem

func process(raw string) (Output, error) {
    h, err := parseHeader(raw)
    if err != nil { return Output{}, err }

    f, err := validate(h)
    if err != nil { return Output{}, err }

    r, err := transform(f)
    if err != nil { return Output{}, err }

    return format(r)
}
  • Half the function is error plumbing
  • The actual logic is buried under boilerplate

The Idea

func process(raw string) error {
    return result.Run(func() {
        h := parseHeader(raw).Expect("parse header")
        f := validate(h).Expect("validate")
        r := transform(f).Expect("transform")
        _ = format(r).Expect("format")
    })
}
  • Expect[T] holds a value or an error — like a typed Result type
  • .Expect("context") unwraps on success, exits on failure

How It Works

flowchart LR
    A[".Expect() sees error"] --> B["panic"]
    B --> C["Run's deferred\nrecover()"]
    C --> D["normal Go error"]
  • result.Run spawns one goroutine and defers recovery
  • Failure exits via panic — caught and returned as a plain error

The One Rule

// ✅ pkg/ library — returns Expect[T], never calls .Expect()
func Parse(raw string) result.Expect[Header] {
    if raw == "" {
        return result.Fail[Header](errors.New("empty input"))
    }
    return result.Ok(Header{raw})
}

// ✅ application code — calls .Expect() inside Run
h := parser.Parse(raw).Expect("parse header")
  • Libraries return Expect[T] — let the caller decide
  • Application code chains .Expect() inside Run

Defers Work Normally

result.Run(func() {
    conn := pool.Acquire()
    defer conn.Release()  // ✅ always fires — success or failure

    score := compute(conn).Expect("compute score")
    _     = save(score).Expect("save result")
})
  • Failure travels as a value — functions return normally
  • Deferred cleanup fires before .Expect() triggers the exit

Debugging: Stack Traces

result.StackTrace(err):

  main.processRequest.func1
      /app/handler.go:42
  main.run.func1
      /app/main.go:17
  • Trace captured at the .Expect() call site — pinpoints the failure
  • result.CaptureStack = false at startup for production builds

Two Build Modes

Default -tags result_goexit
Exit mechanism panic runtime.Goexit
Error path cost ~1.4 µs ~5.5 µs
Stray recover() safe ⚠ re-panic unknowns always safe
  • Panic (default): 4× faster; standard Go recover() idiom applies
  • Goexit (opt-in): immune to accidental error swallowing by any middleware

Performance

Scenario Canonical Go result result (no trace)
Happy path 1 324 ns 1 046 ns 1 027 ns
Error at depth 3 820 ns 1 441 ns 1 149 ns
Error at depth 10 1 257 ns 1 576 ns 1 113 ns

arm64, both sides spawn one goroutine per boundary

  • Happy path: result matches or beats canonical
  • Error path: +300600 ns — rounds to zero next to any I/O

Summary

  • Write functions for the happy path; handle errors once at the boundary
  • pkg/ returns Expect[T] — application code calls .Expect() inside Run
  • Defers, concurrent patterns, and stack traces all work as expected
  • Tune with CaptureStack and build tags if the profile demands it

Best fit: application pipelines with 3+ sequential fallible steps.