package result import ( "errors" "fmt" "path/filepath" "runtime" "strings" ) // Expect holds either a value of type T or an error. // // Fields are unexported to prevent external mutation (e.g. r.Err = nil silently // turning a failure into an apparent success). Methods provide a read-only view // of the same data with no meaningful performance difference — trivial getters // are inlined by the compiler. type Expect[T any] struct { value T err error } // CaptureStack controls whether Expect, Expectf, and Must capture a stack // trace at the failure site. Defaults to true. Set to false at program startup // (before spawning goroutines) to cut ~1.5 KB of allocation and most of the // error-path overhead; errors are still collected and propagated, just without // a trace. StackTrace will return an empty string for errors captured while // this is false. var CaptureStack = true // callers returns the program counters of the call stack starting at the // caller's caller, skipping skip frames above runtime.Callers itself. // Returns nil when CaptureStack is false. func callers(skip int) []uintptr { if !CaptureStack { return nil } var pcs [32]uintptr n := runtime.Callers(skip, pcs[:]) if n == 0 { return nil } cp := make([]uintptr, n) copy(cp, pcs[:n]) return cp } // Ok wraps a successful value in an Expect. func Ok[T any](v T) Expect[T] { return Expect[T]{value: v} } // Err wraps an error in an Expect. func Err[T any](err error) Expect[T] { return Expect[T]{err: err} } // Errf wraps a formatted error in an Expect. It is a convenience shorthand // for [Err][fmt.Errorf(format, args...)]. The caller's file and line are // prepended to the error message automatically. func Errf[T any](format string, args ...any) Expect[T] { _, file, line, _ := runtime.Caller(1) loc := fmt.Sprintf("%s:%d", filepath.Base(file), line) return Expect[T]{err: fmt.Errorf(loc+": "+format, args...)} } // Errw wraps an existing error with a context message, following the standard // Go error-propagation convention (errors.Is/As chain is preserved). Each // wrapping level is placed on its own line so the full error reads as a // top-down trace: outermost context first, root cause last. The caller's file // and line are prepended automatically. // // main.go:42: load config // logger.go:35: parse log level // strconv.Atoi: parsing "x": invalid syntax func Errw[T any](err error, format string, args ...any) Expect[T] { _, file, line, _ := runtime.Caller(1) return Expect[T]{err: fmt.Errorf("%s:%d: %s\n%w", filepath.Base(file), line, fmt.Sprintf(format, args...), 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 } // Value returns the underlying value, or the zero value of T if the Expect // holds an error. Mirrors the (value, error) convention: the caller is trusted // to check [Expect.Err] first, just as they would check the error return in // standard Go code. func (r Expect[T]) Value() T { return r.value } // Must returns the value or panics with the wrapped error and a stack trace. // Use for genuine unrecoverable conditions where an immediate crash is correct. // For normal error propagation inside [Go] or [Run], use [Expect.Expect]. func (r Expect[T]) Must() T { if r.err != nil { panic(&stackError{err: r.err, stack: callers(3)}) } 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 } // Expect returns the value or exits the current goroutine with the wrapped // error annotated with msg. The failure is collected by the enclosing [Go] or // [Run] call as a normal Go error. A 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 { exitGoroutine(&stackError{ err: fmt.Errorf("%s\n%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. // // data := Parse(raw).Expectf("parse user input id=%d", id) func (r Expect[T]) Expectf(format string, args ...any) T { if r.err != nil { exitGoroutine(&stackError{ err: fmt.Errorf("%s\n%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 { ch <- Err[T](se) // Expect/Expectf/Must failure return } panic(v) // genuine runtime panic — crash the program } if finished { ch <- Ok(val) return } if err := collectGoexitFailure(); err != nil { ch <- Err[T](err) } else { ch <- Errf[T]("goroutine exited unexpectedly") } }() val = fn() finished = true }() return ch } // AsyncOf is like [Async] but for functions that already return Expect[T] — // typically library functions that follow the layering rule. This avoids the // double .Expect() that would otherwise be needed: one inside the goroutine to // unwrap, and one on the channel read to re-propagate. // // hostCh := result.AsyncOf(resolveHost) // resolveHost() Expect[string] // portCh := result.AsyncOf(resolvePort) // resolvePort() Expect[int] // return result.Go(func() Config { // host := (<-hostCh).Expect("resolve host") // port := (<-portCh).Expect("resolve port") // return Config{Host: host, Port: port} // }).Unwrap() func AsyncOf[T any](fn func() Expect[T]) <-chan Expect[T] { ch := make(chan Expect[T], 1) go func() { ch <- fn() }() return ch } // Bind partially applies a single-argument function, returning a zero-argument // closure suitable for [AsyncOf]. This hides the closure syntax when the // library function takes one argument: // // hostCh := result.AsyncOf(result.Bind(parseHost, "localhost")) // portCh := result.AsyncOf(result.Bind(parsePort, "8080")) func Bind[T, A any](fn func(A) Expect[T], arg A) func() Expect[T] { return func() Expect[T] { return fn(arg) } } // 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() } // All waits for each channel in order and returns the collected values, or the // first error encountered. Remaining goroutines finish normally — their // channels are buffered so no goroutines are leaked. // // Failure detection is channel-order, not wall-clock: if ch[2] fails before // ch[0] finishes, you still wait for ch[0] first. In practice this rarely // matters — arrange channels so the ones most likely to fail come first and // the two orderings are equivalent. func All[T any](chs ...<-chan Expect[T]) Expect[[]T] { out := make([]T, len(chs)) for i, ch := range chs { r := <-ch if r.err != nil { return Err[[]T](r.err) } out[i] = r.value } return Ok(out) } // Map runs fn on each input in its own goroutine, waits for all to finish, // and returns the results in input order. Unlike [All], every goroutine runs // to completion — all errors are collected and returned together via // errors.Join so callers see the full failure set. func Map[In, Out any](inputs []In, fn func(In) Out) Expect[[]Out] { chs := make([]<-chan Expect[Out], len(inputs)) for i, in := range inputs { chs[i] = Async(func() Out { return fn(in) }) } out := make([]Out, len(inputs)) var errs []error for i, ch := range chs { r := <-ch if r.err != nil { errs = append(errs, r.err) } else { out[i] = r.value } } if len(errs) > 0 { return Err[[]Out](errors.Join(errs...)) } return Ok(out) } // Catch recovers a panic produced by [Expect.Must] and stores it in *errp. // For normal error propagation use [Go] or [Run] instead — they collect // failures from [Expect.Expect] and [Expect.Expectf] regardless of build tag. // 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 call stack captured when [Expect.Expect], // [Expect.Expectf], or [Expect.Must] stored or panicked with an error. // Frames are formatted as "function\n\tfile:line\n" starting at the call // site of the failing Expect/Expectf/Must call. // Returns an empty string if err was not produced by this package or if // [CaptureStack] was false when the error was captured. func StackTrace(err error) string { var s *stackError if !errors.As(err, &s) || len(s.stack) == 0 { return "" } frames := runtime.CallersFrames(s.stack) var b strings.Builder for { f, more := frames.Next() if f.Function != "runtime.goexit" { fmt.Fprintf(&b, "%s\n\t%s:%d\n", f.Function, f.File, f.Line) } if !more { break } } return b.String() } // stackError wraps an error with program counters captured at the failure site. type stackError struct { err error stack []uintptr // nil when CaptureStack is false } func (s *stackError) Error() string { return s.err.Error() } func (s *stackError) Unwrap() error { return s.err }