--- theme: gaia highlightTheme: monokai margin: 0 html: true marp: true --- # `result` ## Happy-Path Error Handling in Go > Focus on success. Handle failure at the boundary. --- ## The Problem ```go 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 ```go 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 ```mermaid 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 ```go // ✅ 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 ```go 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**: +300–600 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.**