//go:build !result_goexit // Default error-exit implementation: Expect and Expectf signal failure via // panic, which is caught by the enclosing result.Go / result.Run goroutine. // // Performance: error path ≈ 1.4 µs / ~352 B per failure (goroutine spawn + // panic + recover). The runtime.Goexit build is about 4× slower (~5.5 µs). // // Trade-off: a deferred recover() inside a Go/Run closure that does not // re-panic unrecognized values will silently swallow a result failure and let // execution continue as if nothing happened. This follows standard Go practice // for recover() — always check the type and re-panic anything unrecognized: // // defer func() { // if r := recover(); r != nil { // if _, ok := r.(MyExpectedType); !ok { // panic(r) // not ours — let it propagate // } // // handle MyExpectedType ... // } // }() // // If your codebase uses bare recover() inside Go/Run closures, or integrates // with frameworks that do, opt into the safer runtime.Goexit implementation: // // go test -tags result_goexit ./... // go build -tags result_goexit ./... package result import ( "fmt" ) // Expect returns the value or panics with the annotated error, which is // collected by the enclosing [Go] or [Run] call. The stack trace is captured // at this call site when [CaptureStack] is true. // // data := Parse(raw).Expect("parse user input") func (r Expect[T]) Expect(msg string) T { if r.err != nil { panic(&stackError{ err: fmt.Errorf("%s: %w", msg, r.err), stack: callers(3), }) } return r.value } // Expectf is like [Expect.Expect] but accepts a fmt.Sprintf-style format string // for the context message. The wrapped error is always appended as ": ". // // data := Parse(raw).Expectf("parse user input id=%d", id) func (r Expect[T]) Expectf(format string, args ...any) T { if r.err != nil { panic(&stackError{ err: fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), r.err), stack: callers(3), }) } return r.value } // Async runs fn in a new goroutine and returns a channel that will receive // exactly one Expect[T] when the goroutine finishes. The channel is buffered // so the goroutine never blocks even if the caller is busy with other work. // // Use Async when you want to run several operations concurrently and collect // their results later: // // aCh := result.Async(func() *A { return buildA().Expect("build A") }) // bCh := result.Async(func() *B { return buildB().Expect("build B") }) // a := (<-aCh).Expect("A") // b := (<-bCh).Expect("B") // // Genuine runtime panics (nil-pointer dereferences, etc.) are not recovered — // they still crash the program, as they should. func Async[T any](fn func() T) <-chan Expect[T] { ch := make(chan Expect[T], 1) go func() { var ( val T finished bool ) defer func() { if v := recover(); v != nil { if se, ok := v.(*stackError); ok { // Expect/Must panic — treat as a collected failure. ch <- Err[T](se) return } panic(v) // genuine runtime panic — crash the program } if finished { ch <- Ok(val) return } ch <- Errf[T]("goroutine exited unexpectedly") }() val = fn() finished = true }() return ch }