OutcomeX is a lightweight, intuitive package designed to handle operations that result in either success or failure. It provides a structured way to manage outcomes, ensuring clarity and reliability in error handling. Whether you’re dealing with expected results or unexpected failures, OutcomeX streamlines the process for better code organization and maintainability.
🔹 Clear and concise error handling 🔹Simplified success/failure flow 🔹 Enhances code readability and structure
Let's rock
A robust implementation of error handling patterns inspired by Rust's Result and .NET's ErrorOr, providing:
- 🚂 Railway-oriented programming
- 🧩 Type-safe error propagation
- ⚡ Async error handling
- 🧠 Composite errors
- 📊 Detailed error metadata
- Chainable operations with
Then/ThenConvert - Pattern matching with
Match/MatchResult - Async workflows with context cancellation
- Error recovery and fallback mechanisms
- Benchmarked against standard error handling
go get github.com/hadrielwonda/outcomexpackage main
import (
"fmt"
"github.com/hadrielwonda/outcomex"
)
func main() {
result := outcomex.WrapOperation(func() (int, error) {
return 42, nil
}).
Then(func(n int) outcomex.Outcome[int] {
return outcomex.Success(n * 2)
}).
MatchResult(
func(v int) any { return v },
func(errs []outcomex.Error) any { return errs },
)
fmt.Println(result) // Output: 84
}| Operation | Time/op | Allocs/op |
|---|---|---|
| Success Path | 15.2 ns/op | 0 allocs/op |
| Failure Path | 18.7 ns/op | 1 allocs/op |
| Standard Error Handling | 12.4 ns/op | 0 allocs/op |
| pkg/errors | 24.1 ns/op | 2 allocs/op |
go test -bench=. -benchmem| Integration | Package | Status |
|---|---|---|
| Gin | outcomex-gin | Active |
| Echo | outcomex-echo | Beta |
| Sentry | outcomex-sentry | Planned |
Apache License - See LICENSE for details
# docs/design.md
# Architecture Decisions
## Core Principles
1. **Immutability**: All Outcome instances are immutable after creation
2. **Composability**: Operations can be chained without side effects
3. **Context Awareness**: Async operations respect context cancellation
4. **Type Safety**: Generic implementation prevents type mismatchestype Outcome[T any] struct {
errors []Error // Contains all accumulated errors
}Async operations use channel-based communication
Context cancellation propagates through async chains
Buffered channels prevent goroutine leaks
Zero-allocation success paths
Interface-free design for compiler optimizations
Pre-sized error slices for common cases
# docs/error-codes.md
# Error Code Conventions
## Standard Codes
Code | Description
-----|------------
validation | Input validation failure
not_found | Resource missing
conflict | State conflict
unexpected | Unhandled exception
timeout | Operation timed out
// Register custom codes at init
func init() {
outcomex.RegisterCode("custom_code", "Custom description")
}Use snake_case for code names
Keep codes domain-specific
Document all codes in package documentation
Use consistent code names across services
# docs/examples.mdtype User struct {
Name string
Age int
}
func validateUser(u User) outcomex.Outcome[User] {
return outcomex.Success(u).
Where(func(u User) bool { return u.Age >= 18 },
outcomex.ValidationError("underage")).
Where(func(u User) bool { return u.Name != "" },
outcomex.ValidationError("empty_name"))
}ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
result := outcomex.Async(ctx, fetchData).
ThenAsync(ctx, processData).
ThenAsync(ctx, storeData).
Await() val := outcomex.Failure[int](outcomex.NotFoundError("user")).
Recover(func(errs []outcomex.Error) int {
return -1 // Default value
})ops := []outcomex.Outcome[any]{
outcomex.WrapOperation(fetchUser),
outcomex.WrapOperation(fetchOrders),
}
combined := outcomex.Combine(ops...)'''go
// internal/outcomex/outcome_test.go
package outcomex
import (
"context"
"errors"
"testing"
"time"
)
func TestSuccess(t *testing.T) {
o := Success(42)
if !o.IsSuccess() {
t.Error("Should be success")
}
if o.Value() != 42 {
t.Error("Incorrect value")
}
}
func TestFailure(t *testing.T) {
o := Failure[int](NewError("test", "error"))
if !o.IsFailure() {
t.Error("Should be failure")
}
if len(o.Errors()) != 1 {
t.Error("Should have one error")
}
}
func TestThenChain(t *testing.T) {
result := Success(2).
Then(func(n int) Outcome[int] { return Success(n * 3) }).
Then(func(n int) Outcome[int] { return Success(n + 1) })
if result.Value() != 7 {
t.Errorf("Expected 7, got %d", result.Value())
}
}
func TestAsyncTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
ao := Async(ctx, func() Outcome[int] {
time.Sleep(20 * time.Millisecond)
return Success(42)
})
result := ao.Await()
if result.IsSuccess() {
t.Error("Should have timed out")
}
}
func TestErrorConversion(t *testing.T) {
_, err := errors.New("standard error")
o := ConvertToOutcome(42, err)
if o.IsSuccess() {
t.Error("Should convert standard error")
}
}
// Benchmarking
func BenchmarkSuccessPath(b *testing.B) {
for i := 0; i < b.N; i++ {
Success(42).Then(func(n int) Outcome[int] {
return Success(n * 2)
})
}
}
func BenchmarkFailurePath(b *testing.B) {
err := NewError("test", "error")
for i := 0; i < b.N; i++ {
Failure[int](err).Then(func(n int) Outcome[int] {
return Success(n * 2)
})
}
}