6.8 KiB
6.8 KiB
| 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.Runspawns one goroutine and defers recovery- Failure exits via
panic— caught and returned as a plainerror
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()insideRun
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 = falseat 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/returnsExpect[T]— application code calls.Expect()insideRun- Defers, concurrent patterns, and stack traces all work as expected
- Tune with
CaptureStackand build tags if the profile demands it
Best fit: application pipelines with 3+ sequential fallible steps.