package result import ( "errors" "fmt" "runtime" "runtime/debug" "sync" ) // Expect holds either a value of type T or an error. type Expect[T any] struct { value T err error } // gErrors stores errors set by Expect/Expectf before calling runtime.Goexit, // keyed by goroutine ID. Entries are consumed by the enclosing Go or Run call. var gErrors sync.Map // goroutineID returns the current goroutine's numeric ID by parsing the first // line of runtime.Stack output ("goroutine NNN [...]"). Called only on error // paths so the runtime.Stack overhead is acceptable. func goroutineID() uint64 { var buf [64]byte n := runtime.Stack(buf[:], false) var id uint64 for _, b := range buf[10:n] { // skip "goroutine " if b < '0' || b > '9' { break } id = id*10 + uint64(b-'0') } return id } // Ok wraps a successful value in an Expect. func Ok[T any](v T) Expect[T] { return Expect[T]{value: v} } // Fail wraps an error in an Expect. func Fail[T any](err error) Expect[T] { return Expect[T]{err: err} } // Of is a convenience constructor that bridges standard Go (value, error) // return signatures: // // result.Of(os.Open("file.txt")).Expect("open config") func Of[T any](v T, err error) Expect[T] { return Expect[T]{value: v, err: err} } // Err returns the wrapped error, or nil on success. func (r Expect[T]) Err() error { return r.err } // Must returns the value or panics with the wrapped error and a stack trace. // Prefer [Expect.Expect] — it exits via runtime.Goexit and is collected // cleanly by [Go] or [Run]. Must is for call sites where a genuine unrecoverable // condition warrants an immediate crash. func (r Expect[T]) Must() T { if r.err != nil { panic(&stackError{err: r.err, stack: debug.Stack()}) } return r.value } // Expect returns the value or exits the current goroutine via runtime.Goexit, // storing the annotated error for collection by the enclosing [Go] or [Run] // call. The stack trace is captured at this call site. // // data := Parse(raw).Expect("parse user input") func (r Expect[T]) Expect(msg string) T { if r.err != nil { gErrors.Store(goroutineID(), &stackError{ err: fmt.Errorf("%s: %w", msg, r.err), stack: debug.Stack(), }) runtime.Goexit() } 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 { gErrors.Store(goroutineID(), &stackError{ err: fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), r.err), stack: debug.Stack(), }) runtime.Goexit() } return r.value } // Unwrap returns the value and error in the standard Go (value, error) form. // Useful at the boundary where you want to re-join normal error-return code. func (r Expect[T]) Unwrap() (T, error) { return r.value, r.err } // 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 ) id := goroutineID() defer func() { if v := recover(); v != nil { if err, ok := v.(*stackError); ok { // Must() panic — treat as a collected failure. ch <- Fail[T](err) return } panic(v) // genuine runtime panic — crash the program } if finished { ch <- Ok(val) return } if stored, ok := gErrors.LoadAndDelete(id); ok { ch <- Fail[T](stored.(error)) } else { ch <- Fail[T](errors.New("goroutine exited unexpectedly")) } }() val = fn() finished = true }() return ch } // Go runs fn in a new goroutine and blocks until it completes, returning the // result as Expect[T]. It is a convenience wrapper around [Async] for the // common single-goroutine case. // // msg := result.Go(func() string { // cfg := loadConfig().Expect("load config") // return greet(cfg).Expect("greet") // }) func Go[T any](fn func() T) Expect[T] { return <-Async(fn) } // Run runs fn in a new goroutine and returns any error produced by // [Expect.Expect] or [Expect.Expectf] calls within fn. Blocks until fn // completes or the goroutine exits. // // func run() error { // return result.Run(func() { // port := parsePort(cfg.Port).Expect("load config port") // _ = port // happy path continues … // }) // } func Run(fn func()) error { return Go(func() struct{} { fn() return struct{}{} }).Err() } // Catch recovers a panic produced by [Expect.Must] and stores it in *errp. // For [Expect.Expect] and [Expect.Expectf] unwinding, use [Go] or [Run] // instead — those exit via runtime.Goexit and are not recoverable by Catch. // Panics that are not errors (e.g. nil-pointer dereferences) are re-panicked. func Catch(errp *error) { v := recover() if v == nil { return } if err, ok := v.(error); ok { *errp = err return } panic(v) // not an error — let it propagate } // StackTrace returns the stack trace captured when [Expect.Expect], // [Expect.Expectf], or [Expect.Must] stored or panicked with an error. // Returns an empty string if err was not produced by this package. func StackTrace(err error) string { var s *stackError if errors.As(err, &s) { return string(s.stack) } return "" } // stackError wraps an error with a stack trace captured at the failure site. type stackError struct { err error stack []byte } func (s *stackError) Error() string { return s.err.Error() } func (s *stackError) Unwrap() error { return s.err }