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 ¶
- Variables
- func Async(f func())
- func AsyncContext(ctx context.Context, f func(ctx context.Context))
- func AsyncContextWithError(ctx context.Context, f func(ctx context.Context) error)
- func AsyncWithError(f func() error)
- func New[T any](cancel ...func()) (future *Future[T], promise *Promise[T])
- type DefaultGoExecutor
- type Executor
- type Future
- func Combine[T any](futures ...*Future[T]) *Future[[]T]
- func CombineContext[T any](ctx context.Context, futures ...*Future[T]) *Future[[]T]
- func CombineContextNoShortCircuit[T any](ctx context.Context, futures ...*Future[T]) *Future[[]T]
- func CombineContextNoShortCircuitWithExecutor[T any](ctx context.Context, exec Executor[[]T], futures ...*Future[T]) *Future[[]T]
- func CombineContextWithExecutor[T any](ctx context.Context, exec Executor[[]T], futures ...*Future[T]) *Future[[]T]
- func CombineNoShortCircuit[T any](futures ...*Future[T]) *Future[[]T]
- func CombineNoShortCircuitWithExecutor[T any](exec Executor[[]T], futures ...*Future[T]) *Future[[]T]
- func CombineWithExecutor[T any](exec Executor[[]T], futures ...*Future[T]) *Future[[]T]
- 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 FlatMapContextWithExecutor[A, B any](ctx context.Context, fut *Future[A], exec Executor[B], 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 Go[T any](fn func() (T, error)) *Future[T]
- func GoContext[T any](ctx context.Context, operation func(context.Context) (T, error)) *Future[T]
- func GoContextWithExecutor[T any](ctx context.Context, exec Executor[T], ...) *Future[T]
- func GoWithExecutor[T any](exec Executor[T], fn func() (T, error)) *Future[T]
- func Map[A, B any](fut *Future[A], transformFunc func(A) (B, error)) *Future[B]
- func MapContext[A, B any](ctx context.Context, fut *Future[A], ...) *Future[B]
- func MapContextWithExecutor[A, B any](ctx context.Context, fut *Future[A], exec Executor[B], ...) *Future[B]
- func MapWithExecutor[A, B any](fut *Future[A], exec Executor[B], transformFunc func(A) (B, error)) *Future[B]
- func NewError[T any](err error) *Future[T]
- func (f *Future[T]) Await() (T, error)
- func (f *Future[T]) AwaitContext(ctx context.Context) (T, error)
- func (f *Future[T]) Cancel()
- func (f *Future[T]) OnError(callback func(error)) *Future[T]
- func (f *Future[T]) OnErrorContext(ctx context.Context, callback func(context.Context, error)) *Future[T]
- func (f *Future[T]) OnResult(callback func(try.Try[T])) *Future[T]
- func (f *Future[T]) OnResultContext(ctx context.Context, callback func(context.Context, try.Try[T])) *Future[T]
- func (f *Future[T]) OnSuccess(callback func(T)) *Future[T]
- func (f *Future[T]) OnSuccessContext(ctx context.Context, callback func(context.Context, T)) *Future[T]
- func (f *Future[T]) ToChannel() <-chan try.Try[T]
- func (f *Future[T]) ToChannelContext(ctx context.Context) <-chan try.Try[T]
- type Promise
Constants ¶
This section is empty.
Variables ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.