future

package
v0.0.0-...-5103540 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 28, 2026 License: MIT Imports: 9 Imported by: 0

README

future

Go Reference

A Go library for type-safe asynchronous programming with Futures and Promises, featuring automatic panic recovery, context support, and functional composition.

Table of Contents

Purpose

The future package provides a Future/Promise implementation for asynchronous programming in Go. It solves common challenges in async code:

  • Type-Safe Async: Generic futures with compile-time type checking
  • Panic Recovery: Automatically catches panics and converts them to errors with stack traces
  • Context Support: Full context integration for cancellation and timeouts
  • Immutability: Promises can only be completed once (first completion wins); results are immutable
  • Functional Composition: Map, FlatMap, and Combine for building complex async workflows
  • Callback System: OnSuccess, OnError, OnResult for reactive programming

Core Concepts

Future (Read-Only Side)

A Future[T] represents the eventual result of an asynchronous computation. It's the "consumer" side:

type Future[T any] struct {
    // Provides read-only access to async results
}

Key Features:

  • Read-only access to results (cannot be completed)
  • Thread-safe concurrent access
  • Memoized results (computed once, cached, reused for all subsequent Await() calls)
  • Multiple waiters supported
Promise (Write-Only Side)

A Promise[T] is used to complete a Future. It's the "producer" side:

type Promise[T any] struct {
    // Provides write-only access for completing the future
}

Key Features:

  • Write-once semantics (first completion wins)
  • Thread-safe concurrent completion
  • Automatically unblocks all waiters
Separation of Concerns

The Future/Promise split prevents consumers from accidentally completing futures:

future, promise := future.New[int]()
// Pass future to consumers (they can only read)
// Keep promise for producers (they can only write)
Panic Recovery

All async operations automatically recover from panics:

fut := future.Go(func() (int, error) {
    panic("something went wrong") // Recovered automatically
})
result, err := fut.Await()
// err contains: "recovered from panic: something went wrong" + stack trace
Context Cancellation

Context-aware operations support cancellation and timeouts:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

fut := future.GoContext(ctx, fetchData)
result, err := fut.AwaitContext(ctx)
if errors.Is(err, context.DeadlineExceeded) {
    // Timeout occurred
}

Installation

go get github.com/amp-labs/amp-common/future

Quick Start

package main

import (
    "context"
    "fmt"
    "time"

    "github.com/amp-labs/amp-common/future"
)

func main() {
    // Create an async computation
    fut := future.Go(func() (string, error) {
        time.Sleep(100 * time.Millisecond)
        return "Hello, Future!", nil
    })

    // Wait for the result
    result, err := fut.Await()
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }

    fmt.Printf("Result: %s\n", result)
}

Basic Usage

Creating Futures

Using Go() - Most Common

// Simplest way - launches goroutine automatically
fut := future.Go(func() (User, error) {
    return db.FetchUser(userID)
})

result, err := fut.Await()

Using GoContext() - With Cancellation

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

fut := future.GoContext(ctx, func(ctx context.Context) (Data, error) {
    return fetchDataWithContext(ctx)
})

result, err := fut.AwaitContext(ctx)

Using New() - Manual Control

// Full control over execution
fut, promise := future.New[int]()

go func() {
    result := expensiveComputation()
    promise.Success(result)
}()

value, err := fut.Await()
Awaiting Results

Blocking Await

fut := future.Go(fetchData)
result, err := fut.Await() // Blocks until complete

Context-Aware Await

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

fut := future.Go(slowOperation)
result, err := fut.AwaitContext(ctx)
if errors.Is(err, context.DeadlineExceeded) {
    // Handle timeout
}

Multiple Awaits (Idempotent)

fut := future.Go(computation)

// All calls return the same memoized result
result1, _ := fut.Await()
result2, _ := fut.Await()
result3, _ := fut.Await()
// result1 == result2 == result3
Completing Promises

Success

_, promise := future.New[string]()
promise.Success("completed")

Failure

_, promise := future.New[User]()
promise.Failure(errors.New("fetch failed"))

Complete (Go-Style)

_, promise := future.New[Data]()
data, err := someFunction()
promise.Complete(data, err) // Handles both success and error
Fire-and-Forget Operations

Async() - Simplest Async Execution

For operations where you don't need to wait for the result or handle errors explicitly, use Async():

// Launch background work without blocking
future.Async(func() {
    updateCache()
    sendAnalytics()
    cleanupTempFiles()
})

// Continues immediately - no need to await
log.Println("Background work started")

Use cases:

  • Logging and analytics
  • Cache updates
  • Non-critical background tasks
  • Fire-and-forget operations

AsyncContext() - With Cancellation Support

For background operations that should respect cancellation, use AsyncContext():

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

// Launch background work that respects context
future.AsyncContext(ctx, func(ctx context.Context) {
    if err := syncDataWithContext(ctx); err != nil {
        // Error is logged automatically
        return
    }

    select {
    case <-ctx.Done():
        log.Println("Sync canceled")
        return
    default:
        log.Println("Sync completed")
    }
})

// Continues immediately

Use cases:

  • Background sync operations
  • Non-blocking cleanup with timeouts
  • Async operations that should respect cancellation
  • Fire-and-forget with graceful shutdown

AsyncWithError() - With Error Return

For background operations that may fail and you want errors logged, use AsyncWithError():

// Launch background work that can return errors
future.AsyncWithError(func() error {
    if err := updateCache(); err != nil {
        return fmt.Errorf("cache update failed: %w", err)
    }

    if err := sendAnalytics(); err != nil {
        return fmt.Errorf("analytics failed: %w", err)
    }

    return nil
})

// Continues immediately - errors are logged automatically
log.Println("Background work started")

Use cases:

  • Background operations that may fail
  • Non-critical tasks where you want error visibility via logs
  • Cache updates or cleanup that should log failures
  • Fire-and-forget operations that need error tracking

AsyncContextWithError() - With Context and Error Return

For background operations that should respect cancellation and may fail, use AsyncContextWithError():

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

// Launch background work that respects context and can return errors
future.AsyncContextWithError(ctx, func(ctx context.Context) error {
    if err := syncDataWithContext(ctx); err != nil {
        return fmt.Errorf("sync failed: %w", err)
    }

    select {
    case <-ctx.Done():
        return ctx.Err()
    default:
        log.Println("Sync completed")
        return nil
    }
})

// Continues immediately - errors are logged automatically

Use cases:

  • Background sync operations that may fail
  • Non-blocking cleanup with timeouts and error handling
  • Async operations that should respect cancellation and log errors
  • Fire-and-forget with graceful shutdown and error tracking

Key Features:

  • Non-blocking: Returns immediately without waiting for completion
  • No boilerplate: No need to create Future, await, or handle results manually
  • Automatic error logging: Panics and errors are caught and logged automatically
  • Thread-safe: Safe to call from multiple goroutines
  • Context support: AsyncContext respects cancellation and deadlines

Comparison with Go()

// ❌ Overkill: Using Go() when you don't need the result
fut := future.Go(func() (struct{}, error) {
    logEvent(event)
    return struct{}{}, nil
})
// Result is never used

// ✅ Better: Use Async for fire-and-forget
future.Async(func() {
    logEvent(event)
})

// ❌ Overkill: Using Go() when you need to return errors but don't need the result
fut := future.Go(func() (struct{}, error) {
    if err := updateCache(); err != nil {
        return struct{}{}, err
    }
    return struct{}{}, nil
})
// Result and error are never checked

// ✅ Better: Use AsyncWithError for fire-and-forget with error logging
future.AsyncWithError(func() error {
    return updateCache()
})

// ❌ Overkill: Using GoContext() for background work
fut := future.GoContext(ctx, func(ctx context.Context) (struct{}, error) {
    syncData(ctx)
    return struct{}{}, nil
})
// Result is never awaited

// ✅ Better: Use AsyncContext for fire-and-forget with context
future.AsyncContext(ctx, func(ctx context.Context) {
    syncData(ctx)
})

// ✅ Or use AsyncContextWithError if you need error logging
future.AsyncContextWithError(ctx, func(ctx context.Context) error {
    return syncData(ctx)
})

Important Notes:

  • Errors and panics are logged but not propagated (fire-and-forget)
  • AsyncWithError and AsyncContextWithError log errors returned by the function
  • If you need the result or explicit error handling, use Go() or GoContext() instead
  • Operations continue running even after the function returns (truly async)
  • For critical operations where you must handle errors, use Go() + Await() or callbacks

Advanced Usage

Functional Transformations

Map - Transform Success Values

// Fetch user ID, then transform to User
idFuture := future.Go(getUserId)
userFuture := future.Map(idFuture, func(id int) (User, error) {
    return fetchUser(id)
})

user, err := userFuture.Await()

MapContext - With Context Support

ctx := context.Background() // In production, use req.Context() or similar

idFuture := future.Go(getUserId)
userFuture := future.MapContext(ctx, idFuture,
    func(ctx context.Context, id int) (User, error) {
        return fetchUserWithContext(ctx, id)
    })

user, err := userFuture.AwaitContext(ctx)

FlatMap - Chain Async Operations

// Fetch user, then fetch their posts (both async)
userFuture := future.Go(fetchUser)
postsFuture := future.FlatMap(userFuture, func(user User) *future.Future[[]Post] {
    return future.Go(func() ([]Post, error) {
        return fetchPosts(user.ID)
    })
})

posts, err := postsFuture.Await()
Combining Multiple Futures

Combine - Wait for All (Short-Circuit on Error)

// ✅ Correct: All futures must return the same type for Combine
fut1 := future.Go(fetchUser1)  // Future[User]
fut2 := future.Go(fetchUser2)  // Future[User]
fut3 := future.Go(fetchUser3)  // Future[User]

combined := future.Combine(fut1, fut2, fut3)
users, err := combined.Await()  // []User
if err != nil {
    // One of the futures failed
}

firstUser := users[0]
secondUser := users[1]
thirdUser := users[2]

// ❌ Won't compile: Mixed types
userFut := future.Go(fetchUser)   // Future[User]
postFut := future.Go(fetchPost)   // Future[Post]
combined := future.Combine(userFut, postFut)  // Compile error!

// ✅ For different types, await separately or use a struct wrapper
type UserAndPosts struct {
    User  User
    Posts []Post
}
wrapper := future.Go(func() (UserAndPosts, error) {
    user, err1 := userFut.Await()
    posts, err2 := postFut.Await()
    if err1 != nil {
        return UserAndPosts{}, err1
    }
    if err2 != nil {
        return UserAndPosts{}, err2
    }
    return UserAndPosts{User: user, Posts: posts}, nil
})

CombineNoShortCircuit - Collect All Errors

futs := make([]*future.Future[Result], len(tasks))
for i, task := range tasks {
    futs[i] = future.Go(task)
}

combined := future.CombineNoShortCircuit(futs...)
results, err := combined.Await()
if err != nil {
    // err contains ALL errors joined together
    // results still contains partial data
}
Custom Executors

Implement custom execution strategies for advanced use cases like rate limiting or worker pools:

// Example: Rate-limited executor
// go get golang.org/x/time/rate
import "golang.org/x/time/rate"

type RateLimitedExecutor[T any] struct {
    limiter *rate.Limiter
}

func NewRateLimitedExecutor[T any](rps int) *RateLimitedExecutor[T] {
    return &RateLimitedExecutor[T]{
        limiter: rate.NewLimiter(rate.Limit(rps), rps),
    }
}

func (e *RateLimitedExecutor[T]) Go(promise *future.Promise[T],
    callback func() (T, error)) {
    go func() {
        // Wait for rate limiter before executing
        _ = e.limiter.Wait(context.Background())
        promise.Complete(callback())
    }()
}

func (e *RateLimitedExecutor[T]) GoContext(ctx context.Context,
    promise *future.Promise[T], callback func(context.Context) (T, error)) {
    go func() {
        // Respect both rate limit and context cancellation
        if err := e.limiter.Wait(ctx); err != nil {
            promise.Failure(err)
            return
        }
        promise.Complete(callback(ctx))
    }()
}

// Use the rate-limited executor
exec := NewRateLimitedExecutor[APIResponse](10) // 10 requests per second
fut := future.GoWithExecutor(exec, func() (APIResponse, error) {
    return callAPI()
})
Converting to Channels

Basic Channel Conversion

fut1 := future.Go(operation1)
fut2 := future.Go(operation2)

// ToChannel returns a buffered channel (size 1) that receives exactly one result
// The buffer ensures the goroutine won't block even if no one is reading yet
select {
case result := <-fut1.ToChannel():
    if result.Error != nil {
        log.Printf("Operation 1 failed: %v", result.Error)
    }
case result := <-fut2.ToChannel():
    if result.Error != nil {
        log.Printf("Operation 2 failed: %v", result.Error)
    }
}

Context-Aware Channel Conversion

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

fut := future.GoContext(ctx, fetchData)

select {
case result := <-fut.ToChannelContext(ctx):
    if errors.Is(result.Error, context.DeadlineExceeded) {
        log.Printf("Operation timed out")
    }
case <-ctx.Done():
    log.Printf("Context canceled externally")
}
When to Use Futures vs Channels

Use Futures when:

  • You need a single eventual value (one result, one error)
  • You want functional composition (Map, FlatMap, Combine)
  • You need automatic panic recovery with stack traces
  • You want callback-based reactive programming
  • You prefer immutable, write-once semantics

Use Channels when:

  • Streaming multiple values over time
  • Implementing producer-consumer patterns
  • Using select for complex coordination
  • You need fine-grained control over send/receive timing
  • Integrating with existing channel-based code

Example - Future for single value:

fut := future.Go(fetchUser)
user, err := fut.Await()

Example - Channel for streaming:

ch := make(chan Update, 10)
go streamUpdates(ch)
for update := range ch {
    process(update)
}
When NOT to Use Futures

Avoid Futures for:

  • Simple synchronous operations - Just call the function directly
  • Very fast operations (< 1μs) - Goroutine overhead dominates (map lookups, simple arithmetic)
  • Operations needing mid-execution cancellation - Use context + channels for fine-grained control
  • Streaming multiple values - Use channels instead
  • Fire-and-forget operations - Use future.Async() for automatic error logging, or plain goroutines if you don't need error handling

Examples of what NOT to do:

// ❌ Overkill: Simple computation doesn't need async
fut := future.Go(func() (int, error) {
    return x + y, nil
})
result, _ := fut.Await()

// ✅ Just compute it directly
result := x + y

// ❌ Overkill: Map lookup is too fast for goroutine overhead
fut := future.Go(func() (string, error) {
    return userCache[userID], nil
})

// ✅ Direct access
name := userCache[userID]

// ❌ Wrong tool: Need to stream multiple values
fut := future.Go(func() ([]Update, error) {
    // Collect all updates first...
})

// ✅ Use channels for streaming
ch := make(chan Update, 10)
go streamUpdates(ch)
for update := range ch {
    process(update)
}

// ❌ Unnecessary: Fire-and-forget doesn't need Future
fut := future.Go(func() (interface{}, error) {
    logToAnalytics(event)
    return nil, nil
})

// ✅ Use Async for fire-and-forget with automatic error logging
future.Async(func() {
    logToAnalytics(event)
})

// ✅ Or AsyncWithError if the operation can fail and you want errors logged
future.AsyncWithError(func() error {
    return logToAnalyticsWithError(event)
})

// ✅ Or just use a goroutine if you don't need error logging
go logToAnalytics(event)

Callbacks

OnSuccess - React to Successful Completion
fut := future.Go(fetchUser)

fut.OnSuccess(func(user User) {
    log.Printf("Successfully fetched user: %s", user.Name)
    metrics.RecordSuccess()
})

// Continues execution...
OnError - React to Errors
fut := future.Go(fetchUser)

fut.OnError(func(err error) {
    log.Printf("Failed to fetch user: %v", err)
    metrics.RecordError()
    alerting.NotifyOnCall()
})
OnResult - Handle Both Cases
fut := future.Go(fetchUser)

fut.OnResult(func(result try.Try[User]) {
    if result.Error != nil {
        log.Printf("Failed: %v", result.Error)
    } else {
        log.Printf("Success: %s", result.Value.Name)
    }
})
Context-Aware Callbacks
ctx := context.Background() // In production, use req.Context() or similar

fut := future.GoContext(ctx, fetchUser)

fut.OnSuccessContext(ctx, func(ctx context.Context, user User) {
    // Callback receives context for DB calls, HTTP requests, etc.
    if err := db.SaveUserContext(ctx, user); err != nil {
        log.ErrorContext(ctx, "Failed to save user", "error", err)
    }
})

fut.OnErrorContext(ctx, func(ctx context.Context, err error) {
    log.ErrorContext(ctx, "Fetch failed", "error", err)
    metrics.RecordError()
})
Method Chaining
future.Go(fetchUser).
    OnSuccess(func(user User) {
        log.Printf("Fetched: %s", user.Name)
    }).
    OnError(func(err error) {
        log.Printf("Error: %v", err)
    }).
    OnResult(func(result try.Try[User]) {
        metrics.RecordCompletion()
    })
Callback Guarantees
  • Invoked exactly once per callback registration
  • Run in separate goroutines (non-blocking)
  • Panic-safe (panics are recovered and do not crash or propagate)
  • No error propagation (callbacks cannot affect the Future's result)
  • Thread-safe (can be registered from any goroutine)
  • Immediate if already complete (registered after completion)

Note on panic handling: If a callback panics, the panic is recovered and does not affect the Future's result or crash the program. The Future's value and error remain unchanged. The panic is caught by the goroutine's recover mechanism but does not propagate to other callbacks or the main Future.

Transformations

The package provides powerful transformation functions for composing async operations:

Map Functions
Function Description Context Support
Map Transform Future[A] → Future[B]
MapContext Transform with context
MapWithExecutor Transform with custom executor
MapContextWithExecutor Transform with both
FlatMap Functions
Function Description Context Support
FlatMap Chain async operations (Future[A] → Future[Future[B]] → Future[B])
FlatMapContext Chain with context
FlatMapWithExecutor Chain with custom executor
FlatMapContextWithExecutor Chain with both
Combine Functions
Function Description Short-Circuit Context
Combine Wait for all futures ✓ (on error)
CombineContext Wait for all with context ✓ (on error)
CombineNoShortCircuit Wait for all, collect errors
CombineContextNoShortCircuit Wait for all, collect errors, with context ✗ (except ctx cancel)

Note: All transformation functions:

  • Automatically propagate errors
  • Support panic recovery
  • Are type-safe with generics
  • Can be chained together

Combining Futures

Parallel Execution Pattern
// Launch all futures first (they run concurrently)
// Note: All futures must return the same type for Combine
fut1 := future.Go(fetchData1)  // Future[Data]
fut2 := future.Go(fetchData2)  // Future[Data]
fut3 := future.Go(fetchData3)  // Future[Data]

// Then combine and wait
combined := future.Combine(fut1, fut2, fut3)
results, err := combined.Await()  // []Data

// All three operations ran in parallel
// Total time ≈ max(time1, time2, time3), not sum
Error Handling Strategies

Fail-Fast (Default)

combined := future.Combine(fut1, fut2, fut3)
results, err := combined.Await()
// Returns immediately on first error
// Remaining futures continue in background

Collect All Errors

combined := future.CombineNoShortCircuit(fut1, fut2, fut3)
results, err := combined.Await()
// Waits for ALL futures to complete
// err contains all errors joined with errors.Join()
Context-Aware Combining
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

fut1 := future.GoContext(ctx, fetchUser)
fut2 := future.GoContext(ctx, fetchPosts)

combined := future.CombineContext(ctx, fut1, fut2)
results, err := combined.AwaitContext(ctx)
if errors.Is(err, context.DeadlineExceeded) {
    // Timeout occurred while waiting
}

Cancellation

Manual Cancellation with Cancel()

The Cancel() method allows you to explicitly cancel a future and trigger its cancellation callback:

fut, promise := future.New[Data](func() {
    // Cleanup function called when Cancel() is invoked
    log.Println("Future cancelled, cleaning up resources")
})

go func() {
    time.Sleep(10 * time.Second)
    promise.Success(data)
}()

// Cancel after 1 second
time.Sleep(1 * time.Second)
fut.Cancel() // Triggers cleanup callback
Cancel() vs Context Cancellation

Use Cancel() when:

  • You need to explicitly abort a specific future
  • You want to trigger cleanup callbacks registered with New()
  • You're managing futures without contexts

Use Context Cancellation when:

  • You need coordinated cancellation across multiple operations
  • You want timeout-based cancellation
  • You're propagating cancellation through call chains
// Context cancellation (preferred for most cases)
ctx, cancel := context.WithCancel(context.Background())
fut := future.GoContext(ctx, fetchData)
cancel() // Cancels the underlying operation

// Manual cancellation (for specific future control)
fut, _ := future.New[Data](cleanupFunc)
fut.Cancel() // Triggers cleanup callback
Cancellation Behavior
  • Cancel() is idempotent: Multiple calls have no additional effect
  • Callbacks still execute: OnSuccess/OnError callbacks run with cancellation error
  • Goroutines may continue: Cancel() doesn't forcefully stop the underlying goroutine
  • Cleanup functions run once: Cancellation callbacks registered with New() execute exactly once

Important: Cancellation is cooperative. The underlying operation must respect context cancellation to actually stop execution.

Best Practices

1. Always Use Context for User-Facing Operations
// Good: respects timeouts and cancellation in HTTP handlers
ctx, cancel := context.WithTimeout(req.Context(), 10*time.Second)
defer cancel()

fut := future.GoContext(ctx, func(ctx context.Context) (Data, error) {
    return fetchDataWithContext(ctx)
})

// Bad: no timeout, could hang forever
fut := future.Go(func() (Data, error) {
    return fetchDataWithoutContext()
})
2. Launch Futures Before Combining
// Good: Futures launched first, easier to debug and inspect
fut1 := future.Go(op1)
fut2 := future.Go(op2)
fut3 := future.Go(op3)
combined := future.Combine(fut1, fut2, fut3)

// Less clear: Inline creation (still concurrent, but harder to debug)
combined := future.Combine(
    future.Go(op1),
    future.Go(op2),
    future.Go(op3),
)
// Note: Both approaches run concurrently, but the first allows
// you to inspect individual futures before combining
3. Use Callbacks for Side Effects
// Good: Non-blocking side effects
future.Go(fetchUser).OnSuccess(func(user User) {
    cache.Set(user.ID, user)
    metrics.RecordSuccess()
})

// Avoid: Blocking just for side effects
user, err := future.Go(fetchUser).Await()
if err == nil {
    cache.Set(user.ID, user)
}
4. Choose the Right Combination Strategy
// Use Combine when you need all to succeed
combined := future.Combine(fut1, fut2, fut3)
// Fails fast on first error

// Use CombineNoShortCircuit when you want partial results
combined := future.CombineNoShortCircuit(fut1, fut2, fut3)
// Collects all errors and results
5. Handle Panics Gracefully
// The package handles panics automatically
fut := future.Go(func() (int, error) {
    // Risky operation - panics are caught automatically
    return riskyOperation()
})

_, err := fut.Await()
if err != nil {
    // Panic has been recovered and converted to error
    // Stack trace included for debugging
    log.Printf("Operation failed: %v", err)
}

// Don't try to recover panics yourself - it's handled
6. Reuse Executors for Custom Behavior
// Create executor once
exec := &MyRateLimitedExecutor[Data]{
    rateLimit: 10, // 10 operations per second
}

// Reuse for multiple operations
fut1 := future.GoWithExecutor(exec, op1)
fut2 := future.GoWithExecutor(exec, op2)
fut3 := future.GoWithExecutor(exec, op3)
// All operations respect the same rate limit
7. Use Type-Safe Error Creation
// Good: Use NewError for pre-failed futures
func fetchUser(id string) *future.Future[User] {
    if id == "" {
        return future.NewError[User](errors.New("id cannot be empty"))
    }
    return future.Go(func() (User, error) {
        return db.GetUser(id)
    })
}

// Maintains consistent async interface

Error Handling

Error Propagation

Errors automatically flow through transformations:

fut1 := future.Go(func() (int, error) {
    return 0, errors.New("source error")
})

fut2 := future.Map(fut1, func(val int) (string, error) {
    // This is never called
    return "transformed", nil
})

_, err := fut2.Await()
// err == "source error" (propagated through Map)
Panic Recovery

Panics are converted to errors with full stack traces:

fut := future.Go(func() (int, error) {
    panic("unexpected panic")
})

_, err := fut.Await()
// err contains:
// - "recovered from panic: unexpected panic"
// - Full stack trace
// - File and line number
Context Errors

Context cancellation and timeouts are treated as errors:

ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
defer cancel()

fut := future.GoContext(ctx, slowOperation)
_, err := fut.AwaitContext(ctx)
// err == context.DeadlineExceeded
Multiple Errors

When combining futures, errors are joined:

fut1 := future.Go(func() (int, error) {
    return 0, errors.New("error 1")
})

fut2 := future.Go(func() (int, error) {
    return 0, errors.New("error 2")
})

combined := future.CombineNoShortCircuit(fut1, fut2)
_, err := combined.Await()
// err contains both errors joined with errors.Join()
Edge Cases

Nil Future:

var fut *future.Future[int]
result, err := fut.Await() // ⚠️ Panics - always create with New() or Go()

Never-Completed Promise:

fut, promise := future.New[int]()
// Never call promise.Complete(), promise.Success(), or promise.Failure()
result, err := fut.Await() // 🚨 BLOCKS FOREVER - production deadlock risk!
                           // This will leak goroutines and hang your application

Critical: Never-completed futures cause goroutine leaks and potential deadlocks. In production code, ALWAYS use AwaitContext with appropriate timeouts to prevent indefinite blocking.

Best Practice: Always use context-aware operations for user-facing code:

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

fut := future.GoContext(ctx, operation)
result, err := fut.AwaitContext(ctx) // ✅ Will timeout instead of blocking forever
Error Identity

Errors preserve their identity through transformations and support errors.Is/errors.As:

var ErrNotFound = errors.New("not found")

fut1 := future.Go(func() (int, error) {
    return 0, ErrNotFound
})

fut2 := future.Map(fut1, func(val int) (string, error) {
    return "transformed", nil
})

_, err := fut2.Await()
errors.Is(err, ErrNotFound) // ✓ true - error identity preserved

Troubleshooting

Issue: Goroutine Leak / Program Hangs

Symptoms:

  • Program doesn't exit cleanly
  • Number of goroutines keeps increasing
  • Application appears to hang indefinitely

Common Causes:

  • Future created but never completed (promise.Complete/Success/Failure never called)
  • Await() called without context timeout
  • Context canceled but operation doesn't respect cancellation

Solutions:

// ❌ Bad: No timeout protection
fut, promise := future.New[Data]()
go someOperation(promise)
result, _ := fut.Await() // May block forever

// ✅ Good: Always use context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

fut := future.GoContext(ctx, func(ctx context.Context) (Data, error) {
    return someOperation(ctx)
})
result, err := fut.AwaitContext(ctx)
if errors.Is(err, context.DeadlineExceeded) {
    // Handle timeout
}
Issue: Panic Not Captured

Symptoms:

  • Application crashes with panic instead of returning error
  • Stack trace shows panic in callback code

Common Cause:

  • Panic occurs in a callback (OnSuccess/OnError/OnResult), not in the Future's main operation

Explanation: Futures automatically recover panics in the main operation, but callbacks run in separate goroutines and their panics are recovered but not propagated to the Future's result.

Solution:

// Callbacks must handle their own panics if needed
fut.OnSuccess(func(user User) {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("Callback panicked: %v", r)
        }
    }()

    riskyCallbackOperation(user)
})
Issue: Context Canceled But Operation Continues

Symptoms:

  • Context is canceled but underlying work still runs
  • Resources not cleaned up when expected
  • Operations complete even after timeout

Common Cause:

  • Context cancellation is cooperative - the underlying operation must actively check for cancellation

Solution:

// ❌ Bad: Doesn't check context
fut := future.GoContext(ctx, func(ctx context.Context) (Data, error) {
    // This will run to completion even if context is canceled
    return longRunningOperation()
})

// ✅ Good: Check context regularly
fut := future.GoContext(ctx, func(ctx context.Context) (Data, error) {
    select {
    case <-ctx.Done():
        return Data{}, ctx.Err()
    default:
    }

    // Use context-aware functions
    return longRunningOperationWithContext(ctx)
})
Issue: "All Goroutines Are Asleep" Deadlock

Symptoms:

  • Fatal error: all goroutines are asleep - deadlock!
  • Program exits with runtime error

Common Causes:

  • Awaiting a future that will never complete
  • Circular dependencies between futures
  • All futures waiting on each other

Solution:

// ❌ Bad: Circular dependency
fut1, promise1 := future.New[int]()
fut2, promise2 := future.New[int]()

go func() {
    val, _ := fut2.Await()
    promise1.Success(val + 1)
}()

go func() {
    val, _ := fut1.Await()
    promise2.Success(val + 1)
}()

// Both futures wait on each other - deadlock!

// ✅ Good: Break the cycle
fut1 := future.Go(func() (int, error) {
    return computeValue1(), nil
})

fut2 := future.FlatMap(fut1, func(val1 int) *future.Future[int] {
    return future.Go(func() (int, error) {
        return computeValue2(val1), nil
    })
})
Issue: Memory Leak with Long-Running Futures

Symptoms:

  • Memory usage increases over time
  • Futures are created but results never accessed
  • Goroutines accumulate in "running" state

Common Cause:

  • Creating futures without awaiting them (orphaned futures)
  • Large result sets cached in memory

Solution:

// ❌ Bad: Creates future but never awaits it
for _, item := range items {
    future.Go(func() (Result, error) {
        return process(item)
    })
    // Future is created but orphaned!
}

// ✅ Good: Await or use callbacks
futures := make([]*future.Future[Result], len(items))
for i, item := range items {
    futures[i] = future.Go(func() (Result, error) {
        return process(item)
    })
}
combined := future.Combine(futures...)
results, err := combined.Await()

// ✅ Alternative: Use callbacks for side effects
for _, item := range items {
    future.Go(func() (Result, error) {
        return process(item)
    }).OnSuccess(func(result Result) {
        handleResult(result)
    })
}
Issue: Race Condition with Shared State

Symptoms:

  • Inconsistent results
  • Data corruption
  • Random failures in concurrent tests

Common Cause:

  • Multiple futures accessing shared state without synchronization

Solution:

// ❌ Bad: Race condition on shared map
results := make(map[string]int)
futures := []*future.Future[int]{...}

for _, fut := range futures {
    fut.OnSuccess(func(val int) {
        results["key"] = val // Race condition!
    })
}

// ✅ Good: Use mutex or channels for synchronization
var mu sync.Mutex
results := make(map[string]int)

for _, fut := range futures {
    fut.OnSuccess(func(val int) {
        mu.Lock()
        defer mu.Unlock()
        results["key"] = val
    })
}

// ✅ Better: Collect results via Combine
combined := future.Combine(futures...)
values, err := combined.Await()
// Process values without races

API Reference

Core Functions
Creating Futures
// Manual control
func New[T any](cancel ...func()) (*Future[T], *Promise[T])

// Automatic execution
func Go[T any](fn func() (T, error)) *Future[T]
func GoContext[T any](ctx context.Context, fn func(context.Context) (T, error)) *Future[T]

// With custom executor
func GoWithExecutor[T any](exec Executor[T], fn func() (T, error)) *Future[T]
func GoContextWithExecutor[T any](ctx context.Context, exec Executor[T],
    fn func(context.Context) (T, error)) *Future[T]

// Pre-failed future
func NewError[T any](err error) *Future[T]

// Fire-and-forget (no result tracking)
func Async(fn func())
func AsyncContext(ctx context.Context, fn func(context.Context))
func AsyncWithError(fn func() error)
func AsyncContextWithError(ctx context.Context, fn func(context.Context) error)
Awaiting Results
// Future methods
func (f *Future[T]) Await() (T, error)
func (f *Future[T]) AwaitContext(ctx context.Context) (T, error)
Completing Promises
// Promise methods
func (p *Promise[T]) Success(value T)
func (p *Promise[T]) Failure(err error)
func (p *Promise[T]) Complete(value T, err error)
Callbacks
// Future methods
func (f *Future[T]) OnSuccess(callback func(T)) *Future[T]
func (f *Future[T]) OnError(callback func(error)) *Future[T]
func (f *Future[T]) OnResult(callback func(try.Try[T])) *Future[T]

// Context-aware versions
func (f *Future[T]) OnSuccessContext(ctx context.Context,
    callback func(context.Context, T)) *Future[T]
func (f *Future[T]) OnErrorContext(ctx context.Context,
    callback func(context.Context, error)) *Future[T]
func (f *Future[T]) OnResultContext(ctx context.Context,
    callback func(context.Context, try.Try[T])) *Future[T]
Transformations
// Map: one-to-one transformation
func Map[A, B any](fut *Future[A], fn func(A) (B, error)) *Future[B]
func MapContext[A, B any](ctx context.Context, fut *Future[A],
    fn func(context.Context, A) (B, error)) *Future[B]
func MapWithExecutor[A, B any](fut *Future[A], exec Executor[B],
    fn func(A) (B, error)) *Future[B]
func MapContextWithExecutor[A, B any](ctx context.Context, fut *Future[A],
    exec Executor[B], fn func(context.Context, A) (B, error)) *Future[B]

// FlatMap: chain async operations
func FlatMap[A, B any](fut *Future[A], fn func(A) *Future[B]) *Future[B]
func FlatMapContext[A, B any](ctx context.Context, fut *Future[A],
    fn func(A) *Future[B]) *Future[B]
func FlatMapWithExecutor[A, B any](fut *Future[A], exec Executor[B],
    fn func(A) *Future[B]) *Future[B]
func FlatMapContextWithExecutor[A, B any](ctx context.Context, fut *Future[A],
    exec Executor[B], fn func(A) *Future[B]) *Future[B]
Combining Futures
// Short-circuit on first error
func Combine[T any](futures ...*Future[T]) *Future[[]T]
func CombineContext[T any](ctx context.Context, futures ...*Future[T]) *Future[[]T]
func CombineWithExecutor[T any](exec Executor[[]T], futures ...*Future[T]) *Future[[]T]
func CombineContextWithExecutor[T any](ctx context.Context, exec Executor[[]T],
    futures ...*Future[T]) *Future[[]T]

// No short-circuit (collect all errors)
func CombineNoShortCircuit[T any](futures ...*Future[T]) *Future[[]T]
func CombineContextNoShortCircuit[T any](ctx context.Context,
    futures ...*Future[T]) *Future[[]T]
func CombineNoShortCircuitWithExecutor[T any](exec Executor[[]T],
    futures ...*Future[T]) *Future[[]T]
func CombineContextNoShortCircuitWithExecutor[T any](ctx context.Context,
    exec Executor[[]T], futures ...*Future[T]) *Future[[]T]
Channel Conversion
// Future methods
func (f *Future[T]) ToChannel() <-chan try.Try[T]
func (f *Future[T]) ToChannelContext(ctx context.Context) <-chan try.Try[T]
Cancellation
// Future method
func (f *Future[T]) Cancel()
Executor Interface
type Executor[T any] interface {
    Go(promise *Promise[T], callback func() (T, error))
    GoContext(ctx context.Context, promise *Promise[T],
        callback func(context.Context) (T, error))
}

// Default implementation
type DefaultGoExecutor[T any] struct{}
func NewDefaultExecutor[T any]() Executor[T]

Examples

Note: The following examples use Go 1.22+ syntax where loop variables are automatically captured per-iteration. If you're using Go < 1.22, create a local copy of the loop variable inside the loop body (e.g., url := url) to avoid capturing issues.

Example 1: Parallel HTTP Requests
urls := []string{
    "https://api.example.com/users/1",
    "https://api.example.com/users/2",
    "https://api.example.com/users/3",
}

// Launch all requests concurrently
// Note: Go 1.22+ captures loop variables per-iteration automatically
futures := make([]*future.Future[*http.Response], len(urls))
for i, url := range urls {
    futures[i] = future.Go(func() (*http.Response, error) {
        return http.Get(url) // 'url' correctly captured per-iteration
    })
}

// Wait for all to complete
combined := future.Combine(futures...)
responses, err := combined.Await()
if err != nil {
    log.Printf("One or more requests failed: %v", err)
    return
}

for i, resp := range responses {
    log.Printf("Response %d: %d", i, resp.StatusCode)
}
Example 2: Chaining Async Operations
ctx := context.Background() // In production, use req.Context() or similar

// Fetch user ID
userIDFuture := future.GoContext(ctx, func(ctx context.Context) (int, error) {
    return getUserID(ctx)
})

// Fetch user details (depends on user ID)
userFuture := future.FlatMapContext(ctx, userIDFuture,
    func(userID int) *future.Future[User] {
        return future.GoContext(ctx, func(ctx context.Context) (User, error) {
            return fetchUser(ctx, userID)
        })
    })

// Fetch user's posts (depends on user)
postsFuture := future.FlatMapContext(ctx, userFuture,
    func(user User) *future.Future[[]Post] {
        return future.GoContext(ctx, func(ctx context.Context) ([]Post, error) {
            return fetchPosts(ctx, user.ID)
        })
    })

posts, err := postsFuture.AwaitContext(ctx)
Example 3: Timeout and Retry
func fetchWithRetry(ctx context.Context, maxAttempts int) (*Data, error) {
    for attempt := 1; attempt <= maxAttempts; attempt++ {
        timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)

        fut := future.GoContext(timeoutCtx, fetchData)
        result, err := fut.AwaitContext(timeoutCtx)
        cancel() // Clean up immediately to prevent context leak

        if err == nil {
            return &result, nil
        }

        if !errors.Is(err, context.DeadlineExceeded) {
            return nil, err // Non-timeout error
        }

        if attempt < maxAttempts {
            log.Printf("Attempt %d timed out, retrying...", attempt)
            time.Sleep(time.Second * time.Duration(attempt))
        }
    }

    return nil, errors.New("all attempts failed")
}
Example 4: Callback-Based Caching
func fetchUserWithCache(userID int) *future.Future[User] {
    // Check cache first
    if cached, ok := cache.Get(userID); ok {
        return future.Go(func() (User, error) {
            return cached.(User), nil
        })
    }

    // Fetch from DB
    fut := future.Go(func() (User, error) {
        return db.FetchUser(userID)
    })

    // Cache successful results asynchronously
    fut.OnSuccess(func(user User) {
        cache.Set(userID, user)
    })

    return fut
}
Example 5: Fan-Out/Fan-In Pattern
func processItems(ctx context.Context, items []Item) ([]Result, error) { // ctx typically from req.Context()
    // Fan-out: launch concurrent processing
    // Note: Go 1.22+ captures loop variables per-iteration automatically
    futures := make([]*future.Future[Result], len(items))
    for i, item := range items {
        futures[i] = future.GoContext(ctx, func(ctx context.Context) (Result, error) {
            return processItem(ctx, item) // 'item' correctly captured per-iteration
        })
    }

    // Fan-in: collect all results
    combined := future.CombineContext(ctx, futures...)
    return combined.AwaitContext(ctx)
}
Example 6: Progress Tracking with Callbacks
func processWithProgress(items []Item) ([]Result, error) {
    var processed atomic.Int32
    total := len(items)

    // Note: Go 1.22+ captures loop variables per-iteration automatically
    futures := make([]*future.Future[Result], total)
    for i, item := range items {
        fut := future.Go(func() (Result, error) {
            return processItem(item) // 'item' correctly captured per-iteration
        })

        // Track progress
        fut.OnResult(func(result try.Try[Result]) {
            count := processed.Add(1)
            percentage := (float64(count) / float64(total)) * 100
            log.Printf("Progress: %.1f%% (%d/%d)", percentage, count, total)
        })

        futures[i] = fut
    }

    combined := future.Combine(futures...)
    return combined.Await()
}

Thread Safety

All operations in this package are thread-safe:

  • Futures can be awaited by multiple goroutines simultaneously
  • Promises can be completed from any goroutine (first completion wins)
  • Callbacks can be registered concurrently with fulfillment
  • Transformations are safe for concurrent use
  • Result memoization uses sync.Once for thread-safe caching
  • Channel operations use mutexes to prevent race conditions

Performance Considerations

  1. Goroutine Overhead: Each Go() call spawns a goroutine (~2KB stack allocation).

    • For very fast operations (< 1μs, e.g., simple arithmetic, map lookups, or struct field access), direct execution may be faster
    • For high-volume operations (>1000/sec, e.g., processing message queues or handling frequent API requests), consider a custom executor with worker pool
    • Example: GoWithExecutor(poolExecutor, fastOp) for rate limiting or resource pooling
    • Trade-off: Goroutines enable true concurrency but add ~2-3μs overhead per spawn
  2. Memory Usage: Futures store results in memory. Large result sets should be handled carefully.

  3. Callback Execution: Callbacks run in separate goroutines, adding overhead. Use for async side effects only.

  4. Executor Reuse: Create custom executors once and reuse them to avoid allocation overhead.

  5. Context Propagation: Context-aware operations have slight overhead but enable proper cancellation.

License

This package is part of amp-common and follows the same license.

Documentation

Overview

Package future provides a Future/Promise implementation for asynchronous programming in Go.

This package follows the split responsibility pattern where Future is the read-only side and Promise is the write-only side of an asynchronous computation. This design prevents consumers from accidentally completing a future they should only be reading from.

Key design principles:

  • Futures are immutable after completion (write-once semantics)
  • All operations are goroutine-safe and can be called from multiple goroutines
  • Results are memoized - once computed, they're stored and reused
  • Panic recovery is built-in to prevent goroutine crashes
  • Context support for cancellation and timeouts

Example usage:

// Using Go() for simple async operations
future := future.Go(func() (int, error) {
    // expensive computation
    return 42, nil
})
result, err := future.Await()

// Using New() for manual control
fut, promise := future.New[string]()
go func() {
    result := someAsyncWork()
    promise.Success(result)
}()
value, err := fut.Await()

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNilFuture is returned when a nil future is provided to a function.
	ErrNilFuture = errors.New("nil future provided to Map")
	// ErrNilFunction is returned when a nil function is provided to a function.
	ErrNilFunction = errors.New("nil function provided to Map")
)

Functions

func Async

func Async(f func())

Async runs the given function asynchronously in a goroutine without blocking. This is a fire-and-forget operation - the caller does not wait for completion or receive a result. Any panics that occur during execution are recovered and logged as errors using the default logger.

Use this when you want to perform work in the background without needing to track its completion or handle results. For context-aware operations, use AsyncContext.

func AsyncContext

func AsyncContext(ctx context.Context, f func(ctx context.Context))

AsyncContext runs the given function asynchronously in a goroutine without blocking, with support for context cancellation. This is a fire-and-forget operation - the caller does not wait for completion or receive a result. Any panics that occur during execution are recovered and logged as errors using the default logger.

The provided context can be used to cancel the async operation or propagate deadlines and values. If the context is canceled, the function may terminate early depending on whether it respects context cancellation.

Use this when you want to perform background work that should respect cancellation signals. For simple async operations without context, use Async.

func AsyncContextWithError

func AsyncContextWithError(ctx context.Context, f func(ctx context.Context) error)

AsyncContextWithError runs the given function asynchronously in a goroutine without blocking, with support for context cancellation and allowing the function to return an error. This is a fire-and-forget operation - the caller does not wait for completion or receive a result. Any errors returned by the function or panics that occur during execution are recovered and logged as errors using the context-aware logger.

The provided context can be used to cancel the async operation or propagate deadlines and values. If the context is canceled, the function may terminate early depending on whether it respects context cancellation.

Use this when you want to perform background work that should respect cancellation signals and may fail. For simple async operations without context, use AsyncWithError.

func AsyncWithError

func AsyncWithError(f func() error)

AsyncWithError runs the given function asynchronously in a goroutine without blocking, allowing the function to return an error. This is a fire-and-forget operation - the caller does not wait for completion or receive a result. Any errors returned by the function or panics that occur during execution are recovered and logged as errors using the default logger.

Use this when you want to perform background work that may fail and you want errors logged, but you don't need to handle the result. For context-aware operations, use AsyncContextWithError.

func New

func New[T any](cancel ...func()) (future *Future[T], promise *Promise[T])

New creates a new Future/Promise pair for manual async computation management.

This function returns both the read-side (Future) and write-side (Promise) of the computation. The caller is responsible for:

  • Managing goroutine lifecycle (New doesn't launch goroutines)
  • Ensuring the promise is eventually fulfilled (via Success, Failure, or Complete)
  • Handling any concurrency concerns in their code

Use this when you need fine-grained control over when and how the computation runs. For simpler cases, use Go() or GoContext() instead.

Example:

fut, promise := future.New[int]()
go func() {
    time.Sleep(time.Second)
    promise.Success(42)
}()
result, err := fut.Await()

Design note: The unbuffered channel is intentionally not closed here - it will be closed by the Promise when fulfill() is called, which broadcasts to all waiters.

Types

type DefaultGoExecutor

type DefaultGoExecutor[T any] struct{}

DefaultGoExecutor is the default implementation of Executor that spawns goroutines for async execution with panic recovery.

func (*DefaultGoExecutor[T]) Go

func (e *DefaultGoExecutor[T]) Go(promise *Promise[T], callback func() (T, error))

Go executes the callback asynchronously in a new goroutine. Any panics in the callback are recovered and converted to errors.

func (*DefaultGoExecutor[T]) GoContext

func (e *DefaultGoExecutor[T]) GoContext(
	ctx context.Context, promise *Promise[T], callback func(ctx context.Context) (T, error),
)

GoContext executes the callback asynchronously with context support. Creates a child context that is canceled when the goroutine completes to prevent leaks. Any panics in the callback are recovered and converted to errors.

type Executor

type Executor[T any] interface {
	// Go executes the callback in a new goroutine and resolves the promise with the result.
	// Panics are recovered and converted to errors.
	Go(promise *Promise[T], callback func() (T, error))

	// GoContext executes the callback with a context in a new goroutine and resolves the promise.
	// The context allows for cancellation and timeout handling.
	GoContext(ctx context.Context, promise *Promise[T], callback func(ctx context.Context) (T, error))
}

Executor is responsible for executing asynchronous operations and resolving promises. It abstracts the goroutine creation and panic recovery logic used by Future/Promise.

type Future

type Future[T any] struct {
	// contains filtered or unexported fields
}

Future represents the read-only side of an asynchronous computation. It provides methods to await the result of a computation that may not yet be complete.

Design notes:

  • The future can only be completed once (enforced by sync.Once)
  • All read operations (Await, AwaitContext) are idempotent and goroutine-safe
  • The result is memoized after first completion
  • Uses a closed channel (resultReady) as a broadcast mechanism for completion
  • Multiple goroutines can safely await the same future

Thread safety:

  • Concurrent calls to Await/AwaitContext are safe
  • The sync.Once ensures the result is only set once
  • The channel pattern allows multiple readers to unblock simultaneously

func Combine

func Combine[T any](futures ...*Future[T]) *Future[[]T]

Combine combines multiple futures into a single future that completes when all inputs complete.

This is useful for waiting on multiple independent async operations that can run in parallel.

Behavior:

  • Waits for ALL futures to complete
  • Returns results in the same order as input futures
  • Short-circuits on first error (doesn't wait for remaining futures)
  • Returns empty slice if no futures provided

IMPORTANT: The futures can be running concurrently - this just waits for them sequentially. To get true parallelism, launch the futures BEFORE calling Combine.

Example:

// Launch three concurrent operations
fut1 := future.Go(fetchUser)
fut2 := future.Go(fetchPosts)
fut3 := future.Go(fetchComments)

// Wait for all to complete
combined := future.Combine(fut1, fut2, fut3)
results, err := combined.Await()
if err != nil {
    // One of the futures failed
}
user, posts, comments := results[0], results[1], results[2]

Design notes:

  • Short-circuiting on error is intentional for fail-fast behavior
  • The futures themselves keep running in background even after error
  • For non-short-circuiting behavior, use CombineNoShortCircuit
  • The goroutine waits sequentially but futures run concurrently

func CombineContext

func CombineContext[T any](ctx context.Context, futures ...*Future[T]) *Future[[]T]

CombineContext is the context-aware version of Combine.

Identical to Combine but with context support, allowing cancellation while waiting.

Behavior:

  • Checks context before awaiting each future
  • Short-circuits on context cancellation OR first error
  • Returns context.Canceled or context.DeadlineExceeded if canceled

Example:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

fut1 := future.GoContext(ctx, fetchUser)
fut2 := future.GoContext(ctx, fetchPosts)
fut3 := future.GoContext(ctx, fetchComments)

combined := future.CombineContext(ctx, fut1, fut2, fut3)
results, err := combined.AwaitContext(ctx)
if errors.Is(err, context.DeadlineExceeded) {
    // Timeout occurred while waiting
}

Design note: The context check happens between awaits to allow early exit if the context is canceled while waiting for slow futures.

func CombineContextNoShortCircuit

func CombineContextNoShortCircuit[T any](ctx context.Context, futures ...*Future[T]) *Future[[]T]

CombineContextNoShortCircuit is the context-aware version of CombineNoShortCircuit.

Waits for ALL futures with context support, collecting all results and errors.

Behavior:

  • Checks context before each future await
  • If context is canceled, immediately returns context error (does NOT wait for remaining futures)
  • If context stays alive, waits for ALL futures and joins all errors

NOTE: Unlike CombineNoShortCircuit, this DOES short-circuit on context cancellation (but not on future errors).

Example:

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

futs := make([]*Future[Result], len(tasks))
for i, task := range tasks {
    futs[i] = future.GoContext(ctx, task)
}

combined := future.CombineContextNoShortCircuit(ctx, futs...)
results, err := combined.AwaitContext(ctx)
if err != nil {
    // Could be context error OR joined future errors
}

Design note: Context cancellation causes immediate return (short-circuit), but future errors do not - all futures are awaited to collect all errors.

func CombineContextNoShortCircuitWithExecutor

func CombineContextNoShortCircuitWithExecutor[T any](
	ctx context.Context, exec Executor[[]T], futures ...*Future[T],
) *Future[[]T]

CombineContextNoShortCircuitWithExecutor combines futures with context support without short-circuiting using a custom executor.

This is identical to CombineContextNoShortCircuit but allows you to specify a custom executor. Most users should use CombineContextNoShortCircuit() instead.

Example:

customExec := &MyCustomExecutor[[]Result]{}
futs := []*Future[Result]{fut1, fut2, fut3}
combined := future.CombineContextNoShortCircuitWithExecutor(ctx, customExec, futs...)

func CombineContextWithExecutor

func CombineContextWithExecutor[T any](
	ctx context.Context, exec Executor[[]T], futures ...*Future[T],
) *Future[[]T]

CombineContextWithExecutor combines multiple futures with context support using a custom executor.

This is identical to CombineContext but allows you to specify a custom executor. Most users should use CombineContext() instead.

Example:

customExec := &MyCustomExecutor[[]User]{}
fut1 := future.GoContext(ctx, fetchUser1)
fut2 := future.GoContext(ctx, fetchUser2)
combined := future.CombineContextWithExecutor(ctx, customExec, fut1, fut2)

func CombineNoShortCircuit

func CombineNoShortCircuit[T any](futures ...*Future[T]) *Future[[]T]

CombineNoShortCircuit combines multiple futures, collecting ALL results and errors.

Unlike Combine, this waits for ALL futures to complete even if some fail. This is useful when you need to know about all failures, or when you want partial results.

Behavior:

  • Waits for ALL futures to complete (never short-circuits)
  • Collects ALL errors and aggregates them with errors.Join
  • Returns ALL results (including zero values for failed futures)
  • If any errors occurred, the combined future fails with joined errors

IMPORTANT: The error case returns BOTH the results AND the error. To access the results when there are errors, you'd need to use the try.Try type directly via the Promise.fulfill mechanism. However, when using Await(), you only get the error.

Example:

// Try to fetch multiple users - we want to know which ones failed
futs := make([]*Future[User], len(ids))
for i, id := range ids {
    futs[i] = future.Go(func() (User, error) { return fetchUser(id) })
}

combined := future.CombineNoShortCircuit(futs...)
results, err := combined.Await()
if err != nil {
    // Multiple errors may have been joined
    log.Printf("Some fetches failed: %v", err)
    // Note: results is nil when using Await() with errors
}

Design note: This uses promise.fulfill with a Try type to store both results and errors, but Await() only returns the error part in failure cases.

func CombineNoShortCircuitWithExecutor

func CombineNoShortCircuitWithExecutor[T any](exec Executor[[]T], futures ...*Future[T]) *Future[[]T]

CombineNoShortCircuitWithExecutor combines multiple futures without short-circuiting using a custom executor.

This is identical to CombineNoShortCircuit but allows you to specify a custom executor. Most users should use CombineNoShortCircuit() instead.

Example:

customExec := &MyCustomExecutor[[]User]{}
futs := []*Future[User]{fut1, fut2, fut3}
combined := future.CombineNoShortCircuitWithExecutor(customExec, futs...)

func CombineWithExecutor

func CombineWithExecutor[T any](exec Executor[[]T], futures ...*Future[T]) *Future[[]T]

CombineWithExecutor combines multiple futures using a custom executor.

This is identical to Combine but allows you to specify a custom executor for the combination operation. Most users should use Combine() instead.

Example:

customExec := &MyCustomExecutor[[]User]{}
fut1 := future.Go(fetchUser1)
fut2 := future.Go(fetchUser2)
combined := future.CombineWithExecutor(customExec, fut1, fut2)

func FlatMap

func FlatMap[A, B any](fut *Future[A], fn func(A) *Future[B]) *Future[B]

FlatMap transforms a Future[A] into a Future[B] by applying a function that returns a Future[B].

This is the "monadic bind" operation for futures. It's used when the transformation itself is asynchronous. The key difference from Map is that fn returns a *Future[B], not a plain B, preventing nested futures (Future[Future[B]]).

Use cases:

  • Chaining multiple async operations sequentially
  • Dependent async calls where the second call needs the result of the first
  • Building complex async workflows

Example:

// Fetch user, then fetch their posts (two async operations)
userFuture := future.Go(fetchUser)
postsFuture := future.FlatMap(userFuture, func(user User) *Future[[]Post] {
    return future.Go(func() ([]Post, error) {
        return fetchPosts(user.ID)
    })
})
posts, err := postsFuture.Await()

Design note: Without FlatMap, you'd get Future[Future[[]Post]] which is awkward. FlatMap "flattens" this to just Future[[]Post] by awaiting both futures.

func FlatMapContext

func FlatMapContext[A, B any](ctx context.Context, fut *Future[A], fn func(A) *Future[B]) *Future[B]

FlatMapContext is the context-aware version of FlatMap.

Identical to FlatMap but with context support throughout the chain.

Example:

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

userFuture := future.GoContext(ctx, fetchUser)
postsFuture := future.FlatMapContext(ctx, userFuture, func(user User) *Future[[]Post] {
    return future.GoContext(ctx, func(ctx context.Context) ([]Post, error) {
        return fetchPostsWithContext(ctx, user.ID)
    })
})
posts, err := postsFuture.AwaitContext(ctx)

Design note: The same context is used for awaiting both futures, allowing cancellation at any point in the chain.

func FlatMapContextWithExecutor

func FlatMapContextWithExecutor[A, B any](
	ctx context.Context, fut *Future[A], exec Executor[B], fn func(A) *Future[B],
) *Future[B]

FlatMapContextWithExecutor transforms a Future[A] into a Future[B] with context support using a custom executor.

This combines context-awareness with custom executor support for chaining async operations. Most users should use FlatMapContext() instead.

Example:

customExec := &MyCustomExecutor[[]Post]{}
userFuture := future.GoContext(ctx, fetchUser)
postsFuture := future.FlatMapContextWithExecutor(ctx, userFuture, customExec, func(user User) *Future[[]Post] {
    return future.GoContext(ctx, func(ctx context.Context) ([]Post, error) {
        return fetchPostsWithContext(ctx, user.ID)
    })
})

func FlatMapWithExecutor

func FlatMapWithExecutor[A, B any](fut *Future[A], exec Executor[B], fn func(A) *Future[B]) *Future[B]

FlatMapWithExecutor transforms a Future[A] into a Future[B] using a custom executor.

This is identical to FlatMap but allows you to specify a custom executor. Most users should use FlatMap() instead.

Example:

customExec := &MyCustomExecutor[[]Post]{}
userFuture := future.Go(fetchUser)
postsFuture := future.FlatMapWithExecutor(userFuture, customExec, func(user User) *Future[[]Post] {
    return future.Go(func() ([]Post, error) {
        return fetchPosts(user.ID)
    })
})

func Go

func Go[T any](fn func() (T, error)) *Future[T]

Go creates a new Future and executes the given function in a new goroutine.

This is the most common way to create a future. It's a convenience wrapper that:

  • Creates a Future/Promise pair
  • Launches a goroutine to execute the function
  • Automatically fulfills the promise with the result
  • Catches any panics and converts them to errors

The panic recovery includes stack traces for debugging, making it safer than raw goroutines while still being convenient.

Example:

future := future.Go(func() (string, error) {
    result, err := http.Get("https://api.example.com")
    if err != nil {
        return "", err
    }
    return result.Body, nil
})
data, err := future.Await()

Design note: The panic recovery is critical because panics in goroutines crash the entire program by default. This converts them to errors that can be handled normally.

func GoContext

func GoContext[T any](ctx context.Context, operation func(context.Context) (T, error)) *Future[T]

GoContext creates a new Future and executes the given function in a goroutine with context support.

This is the context-aware version of Go(). It:

  • Creates a cancellable child context for the goroutine
  • Passes that context to the user's function
  • Automatically cancels the context when the goroutine completes
  • Catches panics and converts them to errors

The child context allows the async operation to be canceled independently and ensures proper cleanup when the computation finishes.

Example:

ctx := context.Background()
future := future.GoContext(ctx, func(ctx context.Context) (Data, error) {
    return fetchDataWithContext(ctx)
})
result, err := future.AwaitContext(ctx)

Design notes:

  • A nil context is automatically replaced with context.Background()
  • The child context (goCtx) is canceled in defer to prevent context leaks
  • The cancel is called even on panic to ensure cleanup
  • The parent context can still cancel the child via the usual context mechanisms

func GoContextWithExecutor

func GoContextWithExecutor[T any](
	ctx context.Context, exec Executor[T], operation func(context.Context) (T, error),
) *Future[T]

GoContextWithExecutor creates a new Future with context support using a custom executor.

This combines the context-awareness of GoContext with the flexibility of custom executors. Most users should use GoContext() instead.

Example:

customExec := &MyCustomExecutor[int]{}
future := future.GoContextWithExecutor(ctx, customExec, func(ctx context.Context) (int, error) {
    return fetchWithContext(ctx)
})

func GoWithExecutor

func GoWithExecutor[T any](exec Executor[T], fn func() (T, error)) *Future[T]

GoWithExecutor creates a new Future and executes the function using a custom executor.

This allows you to customize how the async operation is executed, such as for testing or using a different concurrency model. Most users should use Go() instead.

Example:

customExec := &MyCustomExecutor[int]{}
future := future.GoWithExecutor(customExec, func() (int, error) {
    return 42, nil
})

func Map

func Map[A, B any](fut *Future[A], transformFunc func(A) (B, error)) *Future[B]

Map transforms a Future[A] into a Future[B] by applying a function to the successful result.

This is a functional programming primitive for chaining async operations. It allows you to transform the result of a future without manually handling the error cases.

Behavior:

  • If the original future succeeds, applies fn to the value and returns the result
  • If the original future fails, propagates the error without calling fn
  • If fn returns an error, that error is propagated
  • Launches a new goroutine via Go() to perform the transformation

Example:

// Convert user ID to user object
idFuture := future.Go(getUserId)
userFuture := future.Map(idFuture, func(id int) (User, error) {
    return fetchUser(id)
})
user, err := userFuture.Await()

Design notes:

  • Returns a pre-failed future if inputs are invalid (nil checks)
  • Uses Go() internally, so transformation happens in a separate goroutine
  • Error propagation is automatic - no need for manual if err != nil checks

func MapContext

func MapContext[A, B any](
	ctx context.Context, fut *Future[A], transformFunc func(context.Context, A) (B, error),
) *Future[B]

MapContext is the context-aware version of Map.

This is identical to Map but with context support, allowing:

  • Cancellation of the transformation via context
  • Passing context to the transformation function (e.g., for DB calls)
  • Timeout support for the entire operation

Example:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

idFuture := future.Go(getUserId)
userFuture := future.MapContext(ctx, idFuture, func(ctx context.Context, id int) (User, error) {
    return db.FetchUserWithContext(ctx, id)
})
user, err := userFuture.AwaitContext(ctx)

Design note: Uses GoContext internally to respect the context throughout the operation.

func MapContextWithExecutor

func MapContextWithExecutor[A, B any](
	ctx context.Context,
	fut *Future[A],
	exec Executor[B],
	transformFunc func(context.Context, A) (B, error),
) *Future[B]

MapContextWithExecutor transforms a Future[A] into a Future[B] with context support using a custom executor.

This combines context-awareness with custom executor support for transformations. Most users should use MapContext() instead.

Example:

customExec := &MyCustomExecutor[User]{}
idFuture := future.Go(getUserId)
userFuture := future.MapContextWithExecutor(ctx, idFuture, customExec,
    func(ctx context.Context, id int) (User, error) {
        return fetchUserWithContext(ctx, id)
    })

func MapWithExecutor

func MapWithExecutor[A, B any](
	fut *Future[A],
	exec Executor[B],
	transformFunc func(A) (B, error),
) *Future[B]

MapWithExecutor transforms a Future[A] into a Future[B] using a custom executor.

This is identical to Map but allows you to specify a custom executor for the transformation. Most users should use Map() instead.

Example:

customExec := &MyCustomExecutor[User]{}
idFuture := future.Go(getUserId)
userFuture := future.MapWithExecutor(idFuture, customExec, func(id int) (User, error) {
    return fetchUser(id)
})

func NewError

func NewError[T any](err error) *Future[T]

NewError creates a Future that is already completed with the given error.

This is a convenience function for creating pre-failed futures, which is useful for:

  • Early returns in error cases
  • Validation failures that should be async-compatible
  • Propagating errors through future-based APIs

The returned future is immediately complete, so Await() will return instantly.

Example:

func fetchUser(id string) *Future[User] {
    if id == "" {
        return NewError[User](errors.New("id cannot be empty"))
    }
    return Go(func() (User, error) {
        return db.GetUser(id)
    })
}

func (*Future[T]) Await

func (f *Future[T]) Await() (T, error)

Await blocks until the future completes and returns the result.

This is the primary way to retrieve the result of an async computation.

Behavior:

  • Blocks the calling goroutine until the future completes
  • If already complete, returns immediately with the memoized result
  • Can be called multiple times - always returns the same result
  • Safe for concurrent use by multiple goroutines

The blocking is implemented via channel receive, which is efficient and allows the Go scheduler to do other work while waiting.

Example:

future := future.Go(expensiveComputation)
result, err := future.Await()  // Blocks until complete
result2, err2 := future.Await() // Returns immediately with same result

func (*Future[T]) AwaitContext

func (f *Future[T]) AwaitContext(ctx context.Context) (T, error)

AwaitContext blocks until the future completes or the context is canceled.

This is the context-aware version of Await, allowing for timeouts and cancellation.

Behavior:

  • Returns the result if the future completes first
  • Returns context.Canceled/DeadlineExceeded if context is canceled first
  • If future is already complete, returns result immediately (ignoring context state)
  • If ctx is nil, behaves like Await()
  • Safe for concurrent use by multiple goroutines

IMPORTANT: This does NOT cancel the underlying computation - it only stops waiting. The future will continue computing in the background. If you need to cancel the computation itself, use GoContext and cancel the context you passed to it.

Example:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

future := future.Go(slowComputation)
result, err := future.AwaitContext(ctx)
if errors.Is(err, context.DeadlineExceeded) {
    // Timeout occurred
}

Design note: The select statement races the context cancellation against future completion. Whichever happens first wins. The channel close is instant, so there's no meaningful race condition to worry about.

func (*Future[T]) Cancel

func (f *Future[T]) Cancel()

Cancel attempts to cancel the underlying computation if it supports cancellation.

This is a best-effort operation. If the future was created with GoContext, this will cancel that context. If the future was created with Go or New without a cancellable context, this does nothing.

This simply sends a signal. The actual cancellation depends on whether the underlying operation respects context cancellation. If it doesn't, the operation will continue running in the background.

func (*Future[T]) OnError

func (f *Future[T]) OnError(callback func(error)) *Future[T]

OnError registers a callback to be invoked when the future completes with an error.

The callback is invoked exactly once if the future completes with an error. If the future completes successfully, the callback is never invoked.

Behavior:

  • If the future is already complete with an error, the callback is invoked immediately in a goroutine
  • If the future is already complete successfully, the callback is never invoked
  • If the future is not yet complete, the callback is stored and invoked when fulfillment occurs
  • The callback is always invoked in a separate goroutine to avoid blocking
  • Multiple callbacks can be registered - all will be invoked
  • Nil callbacks are safely ignored

This is useful for error handling side effects like logging, metrics, or cleanup.

Example:

future := future.Go(fetchUser)
future.OnError(func(err error) {
    log.Printf("Failed to fetch user: %v", err)
    metrics.RecordError()
    alerting.NotifyOnCall()
})
future.OnSuccess(func(user User) {
    log.Printf("Successfully fetched user: %s", user.Name)
})

Thread safety: Safe to call from any goroutine, even concurrently with fulfillment.

func (*Future[T]) OnErrorContext

func (f *Future[T]) OnErrorContext(ctx context.Context, callback func(context.Context, error)) *Future[T]

OnErrorContext registers a context-aware callback to be invoked when the future completes with an error.

This is the context-aware version of OnError. The callback receives both the error and a context, which is useful when the callback needs to perform context-dependent error handling like database rollbacks, HTTP requests, or respecting cancellation.

Behavior:

  • If the future is already complete with an error, the callback is invoked immediately in a goroutine
  • If the future is already complete successfully, the callback is never invoked
  • If the future is not yet complete, the callback and context are stored for later invocation
  • The callback is always invoked in a separate goroutine to avoid blocking
  • The context passed at registration time is used when invoking the callback
  • Multiple callbacks can be registered - all will be invoked
  • Nil callbacks are safely ignored
  • Returns the future to allow method chaining

The callback receives:

  • A context (the one provided to OnErrorContext, wrapped in a child context)
  • The error that caused the future to fail

This is useful for error handling side effects that need context, such as:

  • Logging errors with trace IDs from context
  • Recording metrics with context metadata
  • Sending alerts or notifications with timeouts
  • Rolling back transactions with database context
  • Publishing error events to message queues

Example:

future := future.GoContext(ctx, fetchUser)
future.OnErrorContext(ctx, func(ctx context.Context, err error) {
    log.ErrorContext(ctx, "Failed to fetch user", "error", err)
    metrics.RecordError()
    if err := alerting.NotifyContext(ctx, err); err != nil {
        log.WarnContext(ctx, "Failed to send alert", "error", err)
    }
})

Thread safety: Safe to call from any goroutine, even concurrently with fulfillment.

func (*Future[T]) OnResult

func (f *Future[T]) OnResult(callback func(try.Try[T])) *Future[T]

OnResult registers a callback to be invoked when the future completes, regardless of success or error.

The callback is invoked exactly once when the future completes, receiving the full result (both value and error) as a try.Try[T]. This is useful when you need to handle both success and error cases in the same callback, or when you need access to the result type directly.

Behavior:

  • If the future is already complete, the callback is invoked immediately in a goroutine
  • If the future is not yet complete, the callback is stored and invoked when fulfillment occurs
  • The callback is always invoked in a separate goroutine to avoid blocking
  • Multiple callbacks can be registered - all will be invoked
  • Nil callbacks are safely ignored
  • Returns the future to allow method chaining

The callback receives a try.Try[T] which contains:

  • Value: The successful result (if Error is nil)
  • Error: The error (if the future failed)

This is useful for unified result handling or logging that needs both paths.

Example:

future := future.Go(fetchUser)
future.OnResult(func(result try.Try[User]) {
    if result.Error != nil {
        log.Printf("Failed: %v", result.Error)
        metrics.RecordError()
    } else {
        log.Printf("Success: %s", result.Value.Name)
        metrics.RecordSuccess()
    }
})

Thread safety: Safe to call from any goroutine, even concurrently with fulfillment.

func (*Future[T]) OnResultContext

func (f *Future[T]) OnResultContext(ctx context.Context, callback func(context.Context, try.Try[T])) *Future[T]

OnResultContext registers a context-aware callback to be invoked when the future completes.

This is the context-aware version of OnResult. The callback receives both the result and a context, which is useful when the callback needs to perform context-dependent operations like database queries, HTTP requests, or respecting cancellation.

Behavior:

  • If the future is already complete, the callback is invoked immediately in a goroutine
  • If the future is not yet complete, the callback and context are stored for later invocation
  • The callback is always invoked in a separate goroutine to avoid blocking
  • The context passed at registration time is used when invoking the callback
  • Multiple callbacks can be registered - all will be invoked
  • Nil callbacks are safely ignored
  • Returns the future to allow method chaining

The callback receives:

  • A context (the one provided to OnResultContext, wrapped in a child context)
  • A try.Try[T] containing both the value and error

This is useful for callbacks that need context for operations like:

  • Making HTTP requests with timeouts
  • Database queries with context
  • Logging with trace IDs from context
  • Respecting cancellation signals

Example:

future := future.GoContext(ctx, fetchUser)
future.OnResultContext(ctx, func(ctx context.Context, result try.Try[User]) {
    if result.Error != nil {
        log.ErrorContext(ctx, "Failed to fetch user", "error", result.Error)
    } else {
        db.SaveUserContext(ctx, result.Value)
    }
})

Thread safety: Safe to call from any goroutine, even concurrently with fulfillment.

func (*Future[T]) OnSuccess

func (f *Future[T]) OnSuccess(callback func(T)) *Future[T]

OnSuccess registers a callback to be invoked when the future completes successfully.

The callback is invoked exactly once if the future completes with a value (no error). If the future completes with an error, the callback is never invoked.

Behavior:

  • If the future is already complete and successful, the callback is invoked immediately in a goroutine
  • If the future is already complete with an error, the callback is never invoked
  • If the future is not yet complete, the callback is stored and invoked when fulfillment occurs
  • The callback is always invoked in a separate goroutine to avoid blocking
  • Multiple callbacks can be registered - all will be invoked
  • Nil callbacks are safely ignored

This is useful for fire-and-forget side effects that should only occur on success.

Example:

future := future.Go(fetchUser)
future.OnSuccess(func(user User) {
    log.Printf("Successfully fetched user: %s", user.Name)
    metrics.RecordSuccess()
})
future.OnError(func(err error) {
    log.Printf("Failed to fetch user: %v", err)
    metrics.RecordError()
})

Thread safety: Safe to call from any goroutine, even concurrently with fulfillment.

func (*Future[T]) OnSuccessContext

func (f *Future[T]) OnSuccessContext(ctx context.Context, callback func(context.Context, T)) *Future[T]

OnSuccessContext registers a context-aware callback to be invoked when the future completes successfully.

This is the context-aware version of OnSuccess. The callback receives both the successful value and a context, which is useful when the callback needs to perform context-dependent operations like database queries, HTTP requests, or respecting cancellation.

Behavior:

  • If the future is already complete and successful, the callback is invoked immediately in a goroutine
  • If the future is already complete with an error, the callback is never invoked
  • If the future is not yet complete, the callback and context are stored for later invocation
  • The callback is always invoked in a separate goroutine to avoid blocking
  • The context passed at registration time is used when invoking the callback
  • Multiple callbacks can be registered - all will be invoked
  • Nil callbacks are safely ignored
  • Returns the future to allow method chaining

The callback receives:

  • A context (the one provided to OnSuccessContext, wrapped in a child context)
  • The successful value of type T

This is useful for success-only side effects that need context, such as:

  • Saving results to a database with context
  • Making follow-up HTTP requests with timeouts
  • Logging with trace IDs from context
  • Publishing to message queues with cancellation support

Example:

future := future.GoContext(ctx, fetchUser)
future.OnSuccessContext(ctx, func(ctx context.Context, user User) {
    if err := db.SaveUserContext(ctx, user); err != nil {
        log.ErrorContext(ctx, "Failed to save user", "error", err)
    }
    metrics.RecordSuccess()
})

Thread safety: Safe to call from any goroutine, even concurrently with fulfillment.

func (*Future[T]) ToChannel

func (f *Future[T]) ToChannel() <-chan try.Try[T]

ToChannel returns a channel that will receive the result when the future completes.

This bridges futures with Go's channel-based concurrency, allowing you to use futures in select statements and other channel-based workflows.

Behavior:

  • Returns a buffered channel (capacity 1) that receives exactly one result
  • The channel is closed after the result is sent
  • Awaits the future in a separate goroutine (non-blocking)
  • The result is wrapped in a try.Try[T] containing both value and error

Use cases:

  • Combining futures with channels in select statements
  • Integrating with channel-based APIs
  • Converting futures to channels for compatibility
  • Waiting on multiple futures with channels

Example:

fut1 := future.Go(operation1)
fut2 := future.Go(operation2)

select {
case result := <-fut1.ToChannel():
    if result.Error != nil {
        log.Printf("Operation 1 failed: %v", result.Error)
    }
case result := <-fut2.ToChannel():
    if result.Error != nil {
        log.Printf("Operation 2 failed: %v", result.Error)
    }
case <-time.After(5 * time.Second):
    log.Printf("Timeout waiting for operations")
}

Design notes:

  • The channel is buffered to prevent goroutine leaks if receiver stops reading
  • A goroutine is spawned to avoid blocking the caller
  • The channel is closed to signal completion to range loops
  • For context-aware timeout/cancellation, use ToChannelContext instead

func (*Future[T]) ToChannelContext

func (f *Future[T]) ToChannelContext(ctx context.Context) <-chan try.Try[T]

ToChannelContext is the context-aware version of ToChannel.

This bridges futures with Go's channel-based concurrency while respecting context cancellation and timeouts. Use this when you need to convert a future to a channel with cancellation support.

Behavior:

  • Returns a buffered channel (capacity 1) that receives exactly one result
  • The channel is closed after the result is sent
  • Awaits the future with context support in a separate goroutine (non-blocking)
  • If context is canceled before future completes, sends the context error
  • The result is wrapped in a try.Try[T] containing both value and error

Use cases:

  • Combining futures with channels in select statements with timeout
  • Integrating with context-aware channel-based APIs
  • Converting futures to channels with cancellation support
  • Waiting on multiple futures with a shared timeout context

Example with timeout:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

fut := future.GoContext(ctx, fetchData)

select {
case result := <-fut.ToChannelContext(ctx):
    if result.Error != nil {
        if errors.Is(result.Error, context.DeadlineExceeded) {
            log.Printf("Operation timed out")
        } else {
            log.Printf("Operation failed: %v", result.Error)
        }
    } else {
        log.Printf("Success: %v", result.Value)
    }
case <-ctx.Done():
    log.Printf("Context canceled externally")
}

Design notes:

  • Uses AwaitContext internally to respect context cancellation
  • The channel is buffered to prevent goroutine leaks if receiver stops reading
  • A goroutine is spawned to avoid blocking the caller
  • The channel is closed to signal completion to range loops
  • Context cancellation results in a try.Try with context.Canceled/DeadlineExceeded error

type Promise

type Promise[T any] struct {
	// contains filtered or unexported fields
}

Promise represents the write-only side of an asynchronous computation.

A Promise is used to complete a Future by providing either a successful value or an error. It's the "producer" side while Future is the "consumer" side.

Key guarantees:

  • A promise can only be fulfilled once (enforced by sync.Once in the future)
  • Multiple calls to Success/Failure/Complete are safe (later calls are ignored)
  • Fulfillment is thread-safe and can happen from any goroutine
  • Fulfilling a promise unblocks all goroutines waiting on the associated future

Design note: The promise holds a reference to the future, not the other way around. This ensures futures can be passed around without exposing the ability to complete them.

func (*Promise[T]) Complete

func (p *Promise[T]) Complete(value T, err error)

Complete fulfills the promise with a value and error pair.

This is a convenience method that matches Go's standard (value, error) return pattern. It internally calls either Success or Failure based on the error.

Use this when you have both a value and error from a function call, following Go's idiomatic error handling.

Example:

fut, promise := future.New[Data]()
go func() {
    // Function returns (Data, error) tuple
    data, err := fetchData()
    // Complete handles both cases
    promise.Complete(data, err)
}()

Behavior:

  • If err != nil: calls Failure(err), ignoring the value
  • If err == nil: calls Success(value)

Design note: This is the most commonly used method because it matches Go's error handling conventions. It's what Go() uses internally.

Thread safety: Safe to call from any goroutine. If called multiple times, only the first call takes effect.

func (*Promise[T]) Failure

func (p *Promise[T]) Failure(err error)

Failure fulfills the promise with an error.

Use this when the async computation failed and you need to propagate the error.

Example:

fut, promise := future.New[User]()
go func() {
    user, err := fetchUser(id)
    if err != nil {
        promise.Failure(err)
        return
    }
    promise.Success(user)
}()

Design note: The value is set to the zero value of T. This is necessary because the try.Try[T] type requires both a value and error, but only the error matters in the failure case.

Thread safety: Safe to call from any goroutine. If called multiple times, only the first call takes effect.

func (*Promise[T]) IsCancelled

func (p *Promise[T]) IsCancelled() bool

IsCancelled returns true if the promise has been canceled.

This is a thread-safe check that can be called from any goroutine. Once a promise is canceled, it remains canceled permanently.

func (*Promise[T]) Success

func (p *Promise[T]) Success(value T)

Success fulfills the promise with a successful value.

Use this when the async computation succeeded and you have a value to provide.

Example:

fut, promise := future.New[string]()
go func() {
    result := doWork()
    promise.Success(result)
}()

Thread safety: Safe to call from any goroutine. If called multiple times, only the first call takes effect.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL