Documentation
¶
Overview ¶
Package relax provides small helpers for structured, typed panic-based propagation inside well-defined internal boundaries.
Use `FailWith` and `FailOnError*` to escalate errors with optional key/value context without changing function signatures. Use `Check*` helpers at boundary points to recover `Failer` panics back into normal `error` values.
See the package examples in `example_test.go` for runnable usage samples.
Example (ErrorsAs) ¶
package main
import (
"errors"
"fmt"
"github.com/luckyman42/relax"
)
type userNotFoundError struct {
ID int
}
func (e *userNotFoundError) Error() string {
return fmt.Sprintf("user %d not found", e.ID)
}
func main() {
_, err := relax.CheckValue(func() string {
relax.FailWith(&userNotFoundError{ID: 42})
return ""
})
var target *userNotFoundError
fmt.Println(errors.As(err, &target))
fmt.Println(target.ID)
}
Output: true 42
Example (QuickStart) ¶
package main
import (
"errors"
"fmt"
"github.com/luckyman42/relax"
)
func main() {
err := relax.CheckFailer(func() {
relax.FailWith(errors.New("database unavailable"))
})
fmt.Println(err)
}
Output: database unavailable
Example (RealisticServiceFlow) ¶
package main
import (
"errors"
"fmt"
"github.com/luckyman42/relax"
)
func main() {
E := func() error {
return errors.New("storage unavailable")
}
D := func() {
relax.FailWith(E())
}
C := func() { D() }
B := func() { C() }
A := func() error { return relax.CheckFailer(B) }
fmt.Println(A())
}
Output: storage unavailable
Index ¶
- func CheckError(fn func() error) (err error)
- func CheckFailer(fn func()) (err error)
- func CheckResult[T any](fn func() (T, error)) (result T, err error)
- func CheckResult2[T1, T2 any](fn func() (T1, T2, error)) (result1 T1, result2 T2, err error)
- func CheckResult3[T1, T2, T3 any](fn func() (T1, T2, T3, error)) (result1 T1, result2 T2, result3 T3, err error)
- func CheckValue[T any](fn func() T) (result T, err error)
- func CheckValue2[T1, T2 any](fn func() (T1, T2)) (result1 T1, result2 T2, err error)
- func CheckValue3[T1, T2, T3 any](fn func() (T1, T2, T3)) (result1 T1, result2 T2, result3 T3, err error)
- func FailOnError[T any](v T, err error) T
- func FailOnError2[T1, T2 any](v1 T1, v2 T2, err error) (T1, T2)
- func FailOnError3[T1, T2, T3 any](v1 T1, v2 T2, v3 T3, err error) (T1, T2, T3)
- func FailWith(err error, keyVals ...any)
- func HandleFailer(fn func(), onError func(error))
- func IsFailer(err error) bool
- type Failer
Examples ¶
- Package (ErrorsAs)
- Package (QuickStart)
- Package (RealisticServiceFlow)
- CheckError
- CheckFailer
- CheckResult
- CheckResult2
- CheckResult3
- CheckValue
- CheckValue2
- CheckValue3
- ConvertToFailer
- FailOnError
- FailOnError2
- FailOnError3
- FailWith
- FailWith (Context)
- FailWith (Function)
- Failer
- HandleFailer
- HandleFailer (Goroutine)
- IsFailer
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CheckError ¶ added in v0.5.0
CheckError executes fn which returns only an error.
If fn returns a non-nil error, it is returned unchanged. If fn panics with a Failer, the panic is recovered and converted into an error. Any other panic is re-panicked unchanged.
CheckError is used for command-style functions where no value is returned, but execution may still fail.
Example ¶
package main
import (
"errors"
"fmt"
"github.com/luckyman42/relax"
)
func main() {
loadUser := func(id int) (string, error) {
return "", errors.New("user not found")
}
err := relax.CheckError(func() error {
_ = relax.FailOnError(loadUser(99))
return nil
})
fmt.Println(err)
}
Output: user not found
func CheckFailer ¶ added in v0.5.0
func CheckFailer(fn func()) (err error)
CheckFailer executes fn and returns any error produced during execution.
If fn panics with a Failer, the panic is recovered and converted into an error. Any other panic is re-panicked unchanged.
CheckFailer is intended for functions that do not return a value but may fail, and where failures should be handled as errors instead of panics.
Example ¶
package main
import (
"errors"
"fmt"
"github.com/luckyman42/relax"
)
func main() {
err := relax.CheckFailer(func() {
relax.FailWith(errors.New("something failed"))
})
fmt.Println(err)
}
Output: something failed
func CheckResult ¶ added in v0.5.0
CheckResult executes fn which returns a value and an error.
If fn returns a non-nil error, it is returned unchanged. If fn panics with a Failer, the panic is recovered and converted into an error. Any other panic is re-panicked unchanged.
CheckResult is used when the underlying function already follows Go's (T, error) convention but still needs panic-to-error boundary protection.
Example ¶
package main
import (
"errors"
"fmt"
"github.com/luckyman42/relax"
)
func main() {
value, err := relax.CheckResult(func() (int, error) {
if true {
relax.FailWith(errors.New("calculation failed"))
}
return 42, nil
})
fmt.Println(value)
fmt.Println(err)
}
Output: 0 calculation failed
func CheckResult2 ¶ added in v0.5.0
CheckResult2 executes fn which returns two values and an error.
Example ¶
package main
import (
"fmt"
"github.com/luckyman42/relax"
)
func main() {
left, right, err := relax.CheckResult2(func() (int, string, error) {
return 7, "ok", nil
})
fmt.Println(left)
fmt.Println(right)
fmt.Println(err == nil)
}
Output: 7 ok true
func CheckResult3 ¶ added in v0.5.0
func CheckResult3[T1, T2, T3 any](fn func() (T1, T2, T3, error)) (result1 T1, result2 T2, result3 T3, err error)
CheckResult3 executes fn which returns three values and an error.
Example ¶
package main
import (
"fmt"
"github.com/luckyman42/relax"
)
func main() {
major, minor, patch, err := relax.CheckResult3(func() (int, int, int, error) {
return 1, 2, 3, nil
})
fmt.Println(major)
fmt.Println(minor)
fmt.Println(patch)
fmt.Println(err == nil)
}
Output: 1 2 3 true
func CheckValue ¶ added in v0.5.0
CheckValue executes fn and converts its execution into a checked call.
If fn completes successfully, its return value is returned and err is nil. If fn panics with a Failer, the panic is recovered and converted into an error. Any other panic is re-panicked unchanged.
CheckValue is intended for boundary layers (such as HTTP handlers or goroutine entry points) where panics of type Failer should be translated into errors.
Example ¶
Because Failer implements Unwrap, callers can inspect the returned error directly with errors.As without knowing about relax.Failer.
package main
import (
"errors"
"fmt"
"github.com/luckyman42/relax"
)
func fetchUser(id int) (string, error) {
return "", errors.New("database unavailable")
}
func main() {
profile, err := relax.CheckValue(func() string {
return relax.FailOnError(fetchUser(42))
})
fmt.Println(profile == "")
fmt.Println(err)
}
Output: true database unavailable
func CheckValue2 ¶ added in v0.5.0
CheckValue2 executes fn and converts its execution into a checked call for two returned values.
Example ¶
package main
import (
"fmt"
"github.com/luckyman42/relax"
)
func main() {
left, right, err := relax.CheckValue2(func() (int, string) {
return 7, "ok"
})
fmt.Println(left)
fmt.Println(right)
fmt.Println(err == nil)
}
Output: 7 ok true
func CheckValue3 ¶ added in v0.5.0
func CheckValue3[T1, T2, T3 any](fn func() (T1, T2, T3)) (result1 T1, result2 T2, result3 T3, err error)
CheckValue3 executes fn and converts its execution into a checked call for three returned values.
Example ¶
package main
import (
"fmt"
"github.com/luckyman42/relax"
)
func main() {
major, minor, patch, err := relax.CheckValue3(func() (int, int, int) {
return 1, 2, 3
})
fmt.Println(major)
fmt.Println(minor)
fmt.Println(patch)
fmt.Println(err == nil)
}
Output: 1 2 3 true
func FailOnError ¶ added in v0.5.0
FailOnError returns `v` if `err == nil`; otherwise it throws the error via `FailWith(err)`.
This reduces error-forwarding boilerplate inside internal call chains where panic-based propagation is acceptable. Prefer explicit returns in public APIs.
Example ¶
package main
import (
"fmt"
"github.com/luckyman42/relax"
)
func main() {
loadProfile := func() (string, error) {
return "alice", nil
}
profile := relax.FailOnError(loadProfile())
fmt.Println(profile)
}
Output: alice
func FailOnError2 ¶ added in v0.5.0
FailOnError2 returns v1 and v2 if err == nil; otherwise it throws the error via FailWith(err).
Example ¶
package main
import (
"fmt"
"github.com/luckyman42/relax"
)
func main() {
loadName := func() (string, string, error) {
return "Ada", "Lovelace", nil
}
first, last := relax.FailOnError2(loadName())
fmt.Println(first)
fmt.Println(last)
}
Output: Ada Lovelace
func FailOnError3 ¶ added in v0.5.0
FailOnError3 returns v1, v2 and v3 if err == nil; otherwise it throws the error via FailWith(err).
Example ¶
package main
import (
"fmt"
"github.com/luckyman42/relax"
)
func main() {
loadVersion := func() (int, int, int, error) {
return 1, 2, 3, nil
}
major, minor, patch := relax.FailOnError3(loadVersion())
fmt.Println(major)
fmt.Println(minor)
fmt.Println(patch)
}
Output: 1 2 3
func FailWith ¶ added in v0.2.0
FailWith panics with a `Failer` that wraps `err`.
If `err` is already a `Failer` (value or pointer) it will be re-panicked directly; in that case any provided key/value pairs are merged into the existing `Failer.Context`. If `err` is nil, `FailWith` is a no-op.
The `keyVals` are interpreted as alternating key, value pairs. Keys are stringified using `fmt.Sprint`; an odd number of `keyVals` is allowed and the final key will be assigned a `nil` value.
Example ¶
package main
import (
"errors"
"fmt"
"github.com/luckyman42/relax"
)
func main() {
err := relax.CheckFailer(func() {
relax.FailWith(errors.New("save failed"))
})
var failer relax.Failer
if errors.As(err, &failer) {
fmt.Println(failer.Err)
}
}
Output: save failed
Example (Context) ¶
package main
import (
"errors"
"fmt"
"github.com/luckyman42/relax"
)
func main() {
err := relax.CheckFailer(func() {
relax.FailWith(errors.New("save failed"),
"user_id", 42,
"operation", "save_user",
)
})
var failer relax.Failer
if errors.As(err, &failer) {
fmt.Println(failer.Err)
fmt.Println(failer.Context["user_id"])
fmt.Println(failer.Context["operation"])
}
}
Output: save failed 42 save_user
Example (Function) ¶
package main
import (
"errors"
"fmt"
"github.com/luckyman42/relax"
)
func main() {
badFun := func() error {
return errors.New("Failer error")
}
err := relax.CheckFailer(func() {
relax.FailWith(badFun())
})
var failer relax.Failer
if errors.As(err, &failer) {
fmt.Println(failer.Err)
}
}
Output: Failer error
func HandleFailer ¶ added in v0.5.0
func HandleFailer(fn func(), onError func(error))
HandleFailer executes fn inside a CheckFailer boundary and forwards any recovered Failer as a standard error to onError.
HandleFailer is useful for explicit failure-handling boundaries where panic-based propagation is used internally, but failures must be handled locally instead of escaping the current execution flow.
Only panics carrying a Failer are recovered. Any other panic is re-panicked unchanged.
onError must not be nil. Passing a nil handler causes HandleFailer to panic immediately.
Example:
relax.HandleFailer(func() {
process()
}, logger.Error)
In this example, if process panics with a Failer, the panic is recovered and forwarded to logger.Error as a standard error. Any other panic from process is re-panicked unchanged. Could be safely used at the entry point of a goroutine, worker loop, or background job:
go relax.HandleFailer(process, logger.Error)
HandleFailer is especially useful at goroutine entry points, worker loops, background jobs, and asynchronous execution boundaries.
Example ¶
package main
import (
"errors"
"fmt"
"github.com/luckyman42/relax"
)
func main() {
relax.HandleFailer(func() {
relax.FailWith(errors.New("worker failed"))
}, func(err error) {
fmt.Println(err)
})
}
Output: worker failed
Example (Goroutine) ¶
package main
import (
"errors"
"fmt"
"github.com/luckyman42/relax"
)
func main() {
done := make(chan struct{})
go relax.HandleFailer(func() {
relax.FailWith(errors.New("worker failed"))
}, func(err error) {
fmt.Println(err)
close(done)
})
<-done
}
Output: worker failed
func IsFailer ¶ added in v0.3.0
IsFailer reports whether `err` is a `Failer` value or wraps a `Failer`.
Example ¶
package main
import (
"errors"
"fmt"
"github.com/luckyman42/relax"
)
func main() {
failer := relax.ConvertToFailer(errors.New("failure"))
fmt.Println(relax.IsFailer(failer))
fmt.Println(relax.IsFailer(errors.New("normal error")))
}
Output: true false
Types ¶
type Failer ¶ added in v0.2.0
Failer is the public, exported representation of a thrown failure.
A `Failer` preserves the original `error` (Err), a captured stack trace (Stack), the time it was created (Timestamp), and an optional map[string]any Context for arbitrary key/value metadata. The library uses `Failer` values to implement structured panic-based propagation inside trusted internal call chains: callers may `panic` a `Failer` (via `FailWith` or `Failer.Fail`) and a `CheckValue` boundary will convert that panic back into a returned `error`.
Example ¶
package main
import (
"errors"
"fmt"
"github.com/luckyman42/relax"
)
func main() {
err := relax.CheckFailer(func() {
failer := relax.ConvertToFailer(errors.New("repository failed"))
failer.Fail(
"repository", "users",
"operation", "find",
)
})
var failer relax.Failer
if errors.As(err, &failer) {
fmt.Println(failer.Err)
fmt.Println(failer.Context["repository"])
}
}
Output: repository failed users
func ConvertToFailer ¶ added in v0.2.0
ConvertToFailer converts any error into a `Failer` value.
If `err` is already a `Failer` (or wraps one), the underlying `Failer` is returned unchanged. Otherwise a new `Failer` is created capturing the current stack trace and timestamp.
Example ¶
package main
import (
"errors"
"fmt"
"github.com/luckyman42/relax"
)
func main() {
err := errors.New("boom")
failer := relax.ConvertToFailer(err)
fmt.Println(failer.Err)
fmt.Println(failer.Timestamp.IsZero())
fmt.Println(len(failer.Stack) > 0)
}
Output: boom false true