runtime.Goexit() instead of panic
- usage policy: application code vs pkg
This commit is contained in:
parent
09d7c98069
commit
e2b7e94847
@ -39,10 +39,10 @@ tools.versions Pinned tool versions (sourced by Makefile and pre-push
|
|||||||
- **Module imports** — always use the full module path `gitea.djmil.dev/go/template/...`
|
- **Module imports** — always use the full module path `gitea.djmil.dev/go/template/...`
|
||||||
- **Packages** — keep `cmd/` thin (wiring only); business logic belongs in `internal/`
|
- **Packages** — keep `cmd/` thin (wiring only); business logic belongs in `internal/`
|
||||||
- **Types** — expose concrete types from constructors (`New(...) *Type`); never wrap in an interface at the implementation site. Consumers define their own interfaces if they need one (Go's implicit satisfaction makes this free)
|
- **Types** — expose concrete types from constructors (`New(...) *Type`); never wrap in an interface at the implementation site. Consumers define their own interfaces if they need one (Go's implicit satisfaction makes this free)
|
||||||
- **Errors** — `pkg/result` is the default error-handling mechanism for all code in this repo, including public APIs:
|
- **Errors** — `pkg/result` is a convenience tool for removing error-threading clutter from application logic; use it as follows:
|
||||||
- functions return `result.Expect[T]` instead of `(T, error)`
|
- `pkg/` libraries **only return** `result.Expect[T]` — never call `.Expect()`, `.Must()`, or `.Expectf()` inside library code; those methods exit the goroutine via `runtime.Goexit` and are only safe in application-layer code protected by a boundary
|
||||||
- callers unwrap with `.Expect("context")` (panics with annotated error + stack trace) or `.Must()` (panics with raw error)
|
- application code (`cmd/`, HTTP handlers, etc.) chains `.Expect("context")` freely — each call exits the goroutine on failure and is caught at the entry point
|
||||||
- top-level entry points (e.g. `cmd/` functions, HTTP handlers) defer `result.Catch(&err)` to convert any result panic into a normal Go error; genuine runtime panics (nil-deref, etc.) are re-panicked
|
- top-level entry points defer `result.Catch(&err)` (or use `result.Run(...)`) to convert any result exit into a normal Go error; genuine runtime panics (nil-deref, etc.) are re-panicked
|
||||||
- bridge existing `(T, error)` stdlib/third-party calls with `result.Of(...)`: `result.Of(os.ReadFile("cfg.json")).Expect("read config")`
|
- bridge existing `(T, error)` stdlib/third-party calls with `result.Of(...)`: `result.Of(os.ReadFile("cfg.json")).Expect("read config")`
|
||||||
- use `result.StackTrace(err)` to retrieve the capture-site stack from a caught error
|
- use `result.StackTrace(err)` to retrieve the capture-site stack from a caught error
|
||||||
- still use `fmt.Errorf("context: %w", err)` when wrapping errors *before* constructing a `result.Fail`
|
- still use `fmt.Errorf("context: %w", err)` when wrapping errors *before* constructing a `result.Fail`
|
||||||
@ -126,3 +126,4 @@ Debug: use launch config "Debug: app" (F5).
|
|||||||
- 2026-03-29 — Fixed stale docs and .golangci.yml local-prefixes; .vscode launch configs use CLI flags.
|
- 2026-03-29 — Fixed stale docs and .golangci.yml local-prefixes; .vscode launch configs use CLI flags.
|
||||||
- 2026-04-01 — Replaced tools.go/go.mod pinning with tools.versions + go run tool@version; go.mod is now free of dev tool deps.
|
- 2026-04-01 — Replaced tools.go/go.mod pinning with tools.versions + go run tool@version; go.mod is now free of dev tool deps.
|
||||||
- 2026-04-01 — Added make release: lists tags with no args; validates semver, runs test-race+lint+security, then tags+pushes.
|
- 2026-04-01 — Added make release: lists tags with no args; validates semver, runs test-race+lint+security, then tags+pushes.
|
||||||
|
- 2026-04-23 — Documented result layering rule: pkg/ libraries only return Expect[T]; .Expect()/.Must() calls belong in application-layer code.
|
||||||
|
|||||||
15
README.md
15
README.md
@ -182,9 +182,18 @@ keeping `go.mod` clean. It provides `Expect[T]`, a generic type that holds
|
|||||||
either a success value or an error.
|
either a success value or an error.
|
||||||
|
|
||||||
The idea: deep call stacks write for the **happy path**, unwrapping results with
|
The idea: deep call stacks write for the **happy path**, unwrapping results with
|
||||||
`Expect` or `Must` (which panic on failure). A single `defer result.Catch(&err)`
|
`.Expect()` or `.Must()` (which exit the goroutine on failure). A single
|
||||||
at the entry point recovers those panics and returns them as a normal Go error,
|
`defer result.Catch(&err)` at the entry point collects that exit as a normal
|
||||||
with the stack trace captured at the original failure site.
|
Go error, with the stack trace captured at the original failure site.
|
||||||
|
|
||||||
|
### Layering rule
|
||||||
|
|
||||||
|
`pkg/` libraries must only **return** `Expect[T]` — never call `.Expect()` or
|
||||||
|
`.Must()` themselves. Those methods exit the current goroutine via
|
||||||
|
`runtime.Goexit` and are only safe inside application-layer code (`cmd/`,
|
||||||
|
HTTP handlers) that is protected by `defer result.Catch(&err)` or
|
||||||
|
`result.Run(...)`. Calling them inside a reusable package takes control away
|
||||||
|
from the caller and makes the package non-composable.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Wrap any (value, error) function:
|
// Wrap any (value, error) function:
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := run(); err != nil {
|
if err := result.Run(showGreeting); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "fatal: %v\n", err)
|
fmt.Fprintf(os.Stderr, "fatal: %v\n", err)
|
||||||
if stack := result.StackTrace(err); stack != "" {
|
if stack := result.StackTrace(err); stack != "" {
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", stack)
|
fmt.Fprintf(os.Stderr, "%s\n", stack)
|
||||||
@ -24,9 +24,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func run() (err error) {
|
func showGreeting() {
|
||||||
defer result.Catch(&err)
|
|
||||||
|
|
||||||
// ── Config ────────────────────────────────────────────────────────────────
|
// ── Config ────────────────────────────────────────────────────────────────
|
||||||
cfg := config.Load()
|
cfg := config.Load()
|
||||||
|
|
||||||
@ -52,6 +50,4 @@ func run() (err error) {
|
|||||||
log.WithField("message", msg).Info("greeting complete")
|
log.WithField("message", msg).Info("greeting complete")
|
||||||
|
|
||||||
fmt.Printf("%s (listening on :%d)\n", msg, cfg.App.Port)
|
fmt.Printf("%s (listening on :%d)\n", msg, cfg.App.Port)
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,36 @@
|
|||||||
// Package result provides a generic Expect[T] type for happy-path-oriented code.
|
// Package result provides a generic Expect[T] type for happy-path-oriented code.
|
||||||
//
|
//
|
||||||
// The intended pattern is:
|
// # Purpose
|
||||||
//
|
//
|
||||||
// 1. Deep call stacks write for the happy path, using [Expect.Must] or
|
// result is a convenience tool for removing error-threading clutter from
|
||||||
// [Expect.Expect] to unwrap values — panicking on unexpected errors rather
|
// application logic. Instead of propagating (value, error) pairs through every
|
||||||
// than threading error returns through every frame.
|
// frame, functions return Expect[T] and the caller unwraps at the boundary.
|
||||||
// 2. The function that initiates a complex operation defers [Catch], which
|
|
||||||
// recovers any Expect panic and returns it as a normal Go error.
|
|
||||||
//
|
//
|
||||||
// Stack traces are captured at the panic site and can be retrieved from the
|
// # Layering rule
|
||||||
// caught error via [StackTrace].
|
//
|
||||||
|
// Reusable library code (packages under pkg/) must only *return* Expect[T] —
|
||||||
|
// it must never call .Expect(), .Must(), or .Expectf() itself. Those methods
|
||||||
|
// exit the current goroutine via runtime.Goexit and are only safe inside a
|
||||||
|
// goroutine controlled by [Go] or [Run]. Calling them in a library takes
|
||||||
|
// control away from the caller and makes the package non-composable.
|
||||||
|
//
|
||||||
|
// The right split:
|
||||||
|
//
|
||||||
|
// - pkg/ functions: return Expect[T] or Expect[Nothing] — let the caller decide.
|
||||||
|
// - Application code (cmd/, HTTP handlers, …): chain .Expect() calls freely,
|
||||||
|
// protected by a defer result.Catch(&err) or a result.Run wrapper.
|
||||||
|
//
|
||||||
|
// # Intended pattern
|
||||||
|
//
|
||||||
|
// 1. Deep call stacks write for the happy path, using [Expect.Expect] or
|
||||||
|
// [Expect.Expectf] to unwrap values — exiting the current goroutine via
|
||||||
|
// runtime.Goexit on unexpected errors rather than threading error returns
|
||||||
|
// through every frame.
|
||||||
|
// 2. The entry point wraps the work with [Go] or [Run], which spawn the work
|
||||||
|
// in a goroutine and collect any failure as a normal Go error.
|
||||||
|
//
|
||||||
|
// Stack traces are captured at the failure site and can be retrieved from the
|
||||||
|
// collected error via [StackTrace].
|
||||||
//
|
//
|
||||||
// # Constructors
|
// # Constructors
|
||||||
//
|
//
|
||||||
@ -20,14 +41,16 @@
|
|||||||
//
|
//
|
||||||
// # Boundary pattern
|
// # Boundary pattern
|
||||||
//
|
//
|
||||||
// func run() (err error) {
|
// func run() error {
|
||||||
// defer result.Catch(&err)
|
// return result.Run(func() {
|
||||||
// port := parsePort(cfg.Port).Expect("load config port")
|
// port := parsePort(cfg.Port).Expect("load config port")
|
||||||
// _ = port // happy path continues …
|
// _ = port // happy path continues …
|
||||||
// return nil
|
// })
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// [Catch] only intercepts error panics produced by this package. Real runtime
|
// [Go] is the typed variant — it returns Expect[T] when the closure produces
|
||||||
// panics (nil-pointer dereferences, index out of bounds, etc.) are re-panicked
|
// a value. [Run] is a convenience wrapper for closures that return nothing.
|
||||||
// so genuine bugs are never silently swallowed.
|
//
|
||||||
|
// Genuine runtime panics (nil-pointer dereferences, index out of bounds, etc.)
|
||||||
|
// are not recovered — they still crash the program, as they should.
|
||||||
package result
|
package result
|
||||||
|
|||||||
@ -48,18 +48,41 @@ func Example_of() {
|
|||||||
// 9090
|
// 9090
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example_catch shows the boundary pattern: defer Catch at the entry point so
|
// Example_go shows result.Go running a closure in a goroutine and returning
|
||||||
// any Expect/Must panic anywhere in the call stack is captured as a normal
|
// a typed Expect[T] value — the happy-path result or a collected error.
|
||||||
// error return.
|
func Example_go() {
|
||||||
func Example_catch() {
|
port := result.Go(func() int {
|
||||||
run := func() (err error) {
|
return parsePort("8080").Expect("parse port")
|
||||||
defer result.Catch(&err)
|
})
|
||||||
// Simulate a deep call stack: this panics because the port is invalid.
|
|
||||||
_ = parsePort("99999").Expect("load config port")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := run(); err != nil {
|
fmt.Println(port.Expect("main"))
|
||||||
|
// Output:
|
||||||
|
// 8080
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example_goError shows result.Go collecting an error from a failed Expect
|
||||||
|
// call inside the goroutine.
|
||||||
|
func Example_goError() {
|
||||||
|
port := result.Go(func() int {
|
||||||
|
return parsePort("99999").Expect("parse port")
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := port.Err(); err != nil {
|
||||||
|
fmt.Println("failed:", err)
|
||||||
|
}
|
||||||
|
// Output:
|
||||||
|
// failed: parse port: parsePort: 99999 out of range
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example_catch shows the boundary pattern: result.Run collects any
|
||||||
|
// Expect/Expectf failure anywhere in the call stack as a normal error.
|
||||||
|
func Example_catch() {
|
||||||
|
err := result.Run(func() {
|
||||||
|
// Simulate a deep call stack: this exits the goroutine because the port is invalid.
|
||||||
|
_ = parsePort("99999").Expect("load config port")
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
fmt.Println("caught:", err)
|
fmt.Println("caught:", err)
|
||||||
}
|
}
|
||||||
// Output:
|
// Output:
|
||||||
@ -79,25 +102,20 @@ func Example_unwrap() {
|
|||||||
// 443
|
// 443
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example_nonErrorPanic shows that Catch does NOT swallow genuine runtime
|
// Example_mustCollected shows that Must() panics (produced by this package)
|
||||||
// panics — only error panics produced by Must/Expect are captured.
|
// are collected by Go/Run as normal errors, just like Expect failures.
|
||||||
func Example_nonErrorPanic() {
|
// Genuine runtime panics (nil-deref, index out of bounds) are NOT collected —
|
||||||
safeRun := func() (err error) {
|
// they propagate and crash the program, as they should.
|
||||||
defer func() {
|
func Example_mustCollected() {
|
||||||
// Outer recover catches the re-panic from Catch.
|
err := result.Run(func() {
|
||||||
if v := recover(); v != nil {
|
result.Fail[int](errors.New("unrecoverable")).Must()
|
||||||
err = fmt.Errorf("non-error panic: %v", v)
|
})
|
||||||
}
|
|
||||||
}()
|
|
||||||
defer result.Catch(&err)
|
|
||||||
panic("unexpected runtime problem") // not an error — Catch re-panics
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := safeRun(); err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println("collected:", err)
|
||||||
}
|
}
|
||||||
// Output:
|
// Output:
|
||||||
// non-error panic: unexpected runtime problem
|
// collected: unrecoverable
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example_expectf shows Expectf for context messages that include runtime
|
// Example_expectf shows Expectf for context messages that include runtime
|
||||||
@ -112,13 +130,11 @@ func Example_expectf() {
|
|||||||
// Example_expectfError shows that Expectf annotates the error message with the
|
// Example_expectfError shows that Expectf annotates the error message with the
|
||||||
// formatted context, just like Expect does.
|
// formatted context, just like Expect does.
|
||||||
func Example_expectfError() {
|
func Example_expectfError() {
|
||||||
run := func() (err error) {
|
err := result.Run(func() {
|
||||||
defer result.Catch(&err)
|
|
||||||
_ = parsePort("99999").Expectf("arg %d port value", 2)
|
_ = parsePort("99999").Expectf("arg %d port value", 2)
|
||||||
return nil
|
})
|
||||||
}
|
|
||||||
|
|
||||||
if err := run(); err != nil {
|
if err != nil {
|
||||||
fmt.Println("caught:", err)
|
fmt.Println("caught:", err)
|
||||||
}
|
}
|
||||||
// Output:
|
// Output:
|
||||||
|
|||||||
@ -3,7 +3,9 @@ package result
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"runtime"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Expect holds either a value of type T or an error.
|
// Expect holds either a value of type T or an error.
|
||||||
@ -12,6 +14,26 @@ type Expect[T any] struct {
|
|||||||
err error
|
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.
|
// Ok wraps a successful value in an Expect.
|
||||||
func Ok[T any](v T) Expect[T] {
|
func Ok[T any](v T) Expect[T] {
|
||||||
return Expect[T]{value: v}
|
return Expect[T]{value: v}
|
||||||
@ -36,8 +58,9 @@ func (r Expect[T]) Err() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Must returns the value or panics with the wrapped error and a stack trace.
|
// Must returns the value or panics with the wrapped error and a stack trace.
|
||||||
// Prefer [Expect.Expect] — it adds a message that makes the panic site easy to
|
// Prefer [Expect.Expect] — it exits via runtime.Goexit and is collected
|
||||||
// locate in logs.
|
// 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 {
|
func (r Expect[T]) Must() T {
|
||||||
if r.err != nil {
|
if r.err != nil {
|
||||||
panic(&stackError{err: r.err, stack: debug.Stack()})
|
panic(&stackError{err: r.err, stack: debug.Stack()})
|
||||||
@ -45,16 +68,18 @@ func (r Expect[T]) Must() T {
|
|||||||
return r.value
|
return r.value
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expect returns the value or panics with the error annotated by msg and a
|
// Expect returns the value or exits the current goroutine via runtime.Goexit,
|
||||||
// stack trace captured at this call site.
|
// 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")
|
// data := Parse(raw).Expect("parse user input")
|
||||||
func (r Expect[T]) Expect(msg string) T {
|
func (r Expect[T]) Expect(msg string) T {
|
||||||
if r.err != nil {
|
if r.err != nil {
|
||||||
panic(&stackError{
|
gErrors.Store(goroutineID(), &stackError{
|
||||||
err: fmt.Errorf("%s: %w", msg, r.err),
|
err: fmt.Errorf("%s: %w", msg, r.err),
|
||||||
stack: debug.Stack(),
|
stack: debug.Stack(),
|
||||||
})
|
})
|
||||||
|
runtime.Goexit()
|
||||||
}
|
}
|
||||||
return r.value
|
return r.value
|
||||||
}
|
}
|
||||||
@ -65,10 +90,11 @@ func (r Expect[T]) Expect(msg string) T {
|
|||||||
// data := Parse(raw).Expectf("parse user input id=%d", id)
|
// data := Parse(raw).Expectf("parse user input id=%d", id)
|
||||||
func (r Expect[T]) Expectf(format string, args ...any) T {
|
func (r Expect[T]) Expectf(format string, args ...any) T {
|
||||||
if r.err != nil {
|
if r.err != nil {
|
||||||
panic(&stackError{
|
gErrors.Store(goroutineID(), &stackError{
|
||||||
err: fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), r.err),
|
err: fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), r.err),
|
||||||
stack: debug.Stack(),
|
stack: debug.Stack(),
|
||||||
})
|
})
|
||||||
|
runtime.Goexit()
|
||||||
}
|
}
|
||||||
return r.value
|
return r.value
|
||||||
}
|
}
|
||||||
@ -79,18 +105,86 @@ func (r Expect[T]) Unwrap() (T, error) {
|
|||||||
return r.value, r.err
|
return r.value, r.err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Catch recovers a panic produced by [Expect.Must] or [Expect.Expect] and
|
// Async runs fn in a new goroutine and returns a channel that will receive
|
||||||
// stores it in *errp. Call it via defer at the entry point of any function
|
// exactly one Expect[T] when the goroutine finishes. The channel is buffered
|
||||||
// that runs a happy-path call stack:
|
// so the goroutine never blocks even if the caller is busy with other work.
|
||||||
//
|
//
|
||||||
// func run() (err error) {
|
// Use Async when you want to run several operations concurrently and collect
|
||||||
// defer result.Catch(&err)
|
// 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 {
|
||||||
// The stored error retains its stack trace; retrieve it with [StackTrace].
|
return Go(func() struct{} {
|
||||||
// Panics that are not errors (e.g. nil-pointer dereferences) are re-panicked
|
fn()
|
||||||
// so genuine bugs are not silently swallowed.
|
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) {
|
func Catch(errp *error) {
|
||||||
v := recover()
|
v := recover()
|
||||||
if v == nil {
|
if v == nil {
|
||||||
@ -103,9 +197,9 @@ func Catch(errp *error) {
|
|||||||
panic(v) // not an error — let it propagate
|
panic(v) // not an error — let it propagate
|
||||||
}
|
}
|
||||||
|
|
||||||
// StackTrace returns the stack trace captured when [Expect.Expect] or
|
// StackTrace returns the stack trace captured when [Expect.Expect],
|
||||||
// [Expect.Must] panicked. Returns an empty string if err was not produced by
|
// [Expect.Expectf], or [Expect.Must] stored or panicked with an error.
|
||||||
// this package.
|
// Returns an empty string if err was not produced by this package.
|
||||||
func StackTrace(err error) string {
|
func StackTrace(err error) string {
|
||||||
var s *stackError
|
var s *stackError
|
||||||
if errors.As(err, &s) {
|
if errors.As(err, &s) {
|
||||||
@ -114,7 +208,7 @@ func StackTrace(err error) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// stackError wraps an error with a stack trace captured at the panic site.
|
// stackError wraps an error with a stack trace captured at the failure site.
|
||||||
type stackError struct {
|
type stackError struct {
|
||||||
err error
|
err error
|
||||||
stack []byte
|
stack []byte
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user