How fabrica-util's Future Primitive Enables Asynchronous Operations in go-pantheon

fabrica-util's Future[T] is a generic, thread-safe placeholder that allows the go-pantheon framework to execute work concurrently without blocking callers, while providing deterministic error handling, cancellation, timeouts, and composable pipelines.

The fabrica-util library provides essential synchronization primitives for the go-pantheon ecosystem. Its Future generic type serves as the foundational building block for managing asynchronous computations, offering a type-safe alternative to raw channels while preserving Go's concurrency idioms. This primitive enables developers to spawn background work, retrieve results safely, and construct complex asynchronous pipelines without sacrificing error handling or cancellation capabilities.

Core Architecture and State Management

In xsync/future.go (lines 16-23), the Future[T] struct is defined as a generic container holding a value of type T and an error. According to the go-pantheon source code, all fields are protected by a sync.RWMutex to ensure thread-safe concurrent access across multiple goroutines. The implementation uses a dedicated done channel that is closed exactly once when the future completes, serving as a lock-free completion signal that efficiently broadcasts readiness to any number of waiting consumers.

Thread-Safe Result Storage

The internal state management combines a sync.RWMutex with a closed-channel pattern. The mutex guards the value T and err error fields against race conditions during writes, while the done channel provides a memory-efficient mechanism for blocking until completion. This dual approach allows the Future to support both concurrent completion attempts and multiple concurrent readers without data races.

The Completion Signal Pattern

When Complete(value, err) is invoked in xsync/future.go (lines 32-46), it records the result, marks the future as finished, and closes the done channel. This closure signals all waiting goroutines simultaneously without requiring active polling or condition variables. The channel-based signaling ensures that consumers block efficiently using the runtime's scheduler rather than consuming CPU cycles.

Lifecycle Methods

Creating Futures with NewFuture

The NewFuture[T]() constructor allocates the struct and initializes the done channel in xsync/future.go (lines 25-30). This factory pattern ensures every future starts in a consistent, empty state ready to receive a result. The generic type parameter T allows compile-time type safety, eliminating the need for interface{} type assertions common in channel-based approaches.

Idempotent Completion

The Complete(value T, err error) method provides idempotent semantics critical for concurrent environments. As implemented in the fabrica-util source, subsequent calls to Complete are silently ignored after the first invocation, preventing race conditions where multiple producers might attempt to finalize the same result. This safety guarantee simplifies error handling in distributed asynchronous workflows.

Asynchronous Retrieval Patterns

Blocking Waits with Get

The Get() method blocks on <-f.done until the future completes, then returns the stored value and error. This provides a simple synchronous interface to asynchronous operations, converting concurrent execution back into sequential flow when needed. The method safely acquires the read lock after the channel receive to access the final result state.

Cancellation and Timeout Support

For production environments requiring deadlines or cancellation, the implementation provides GetWithContext(ctx context.Context) and GetWithTimeout(d time.Duration) in xsync/future.go (lines 48-78). These methods respect context deadlines and propagate context.Canceled or context.DeadlineExceeded errors appropriately. GetWithTimeout creates an internal timeout context and delegates to GetWithContext, ensuring consistent error handling across all waiting strategies.

Advanced Composition and Cancellation

Pipeline Construction with Then

The Then(f Future[T], fn func(T) (U, error)) function enables functional composition of asynchronous operations. As defined in xsync/future.go (lines 93-112), it spawns a new goroutine that waits for the input future to complete, applies the transformation function, and stores the result in a new Future[U]. This pattern supports pipeline-style architectures without nested callbacks, allowing developers to chain dependent asynchronous operations while maintaining type safety across transformations.

Safe Goroutine Integration

The chaining mechanism relies on the framework-wide Go helper defined in xsync/routines.go (lines 48-85). This utility runs closures in dedicated goroutines with automatic panic recovery and structured error logging. By leveraging Go, the Then operation remains non-blocking and crash-resistant, ensuring that failures in one pipeline stage do not corrupt the entire asynchronous workflow or crash the application.

Explicit Cancellation

The Cancel() method in xsync/future.go (lines 88-91) completes the future with the sentinel error ErrFutureCancelled. This allows upstream callers to abort long-running operations gracefully, with all waiting consumers receiving the cancellation signal immediately. The error handling integrates seamlessly with Go's standard error patterns, allowing applications to distinguish between cancellation and other failure modes.

Practical Implementation Example

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/go-pantheon/fabrica-util/xsync"
)

func main() {
	// 1️⃣ Create a future that will hold an int
	f := xsync.NewFuture[int]()

	// 2️⃣ Run the asynchronous work (e.g. network call) in a separate goroutine
	xsync.Go("load-data", func() error {
		// Simulate latency
		time.Sleep(200 * time.Millisecond)

		// On success:
		f.Complete(42, nil)

		// On failure you could do:
		// f.Complete(0, errors.New("failed"))
		return nil
	})

	// 3️⃣ Wait with a timeout – the caller stays responsive
	val, err := f.GetWithTimeout(500 * time.Millisecond)
	if err != nil {
		fmt.Println("operation failed:", err)
		return
	}
	fmt.Println("result:", val) // → result: 42

	// -------------------------------------------------
	// 4️⃣ Chaining with Then – build a pipeline
	upper := xsync.Then(f, func(v int) (string, error) {
		return fmt.Sprintf("value=%d", v), nil
	})

	// 5️⃣ The chained future can be awaited independently
	s, _ := upper.Get()
	fmt.Println(s) // → value=42

	// -------------------------------------------------
	// 6️⃣ Context‑aware waiting – abort if caller cancels
	ctx, cancel := context.WithCancel(context.Background())
	go func() {
		time.Sleep(100 * time.Millisecond)
		cancel() // simulate external cancellation
	}()

	_, err = f.GetWithContext(ctx)
	fmt.Println("canceled:", err) // → canceled: context canceled
}

Summary

  • fabrica-util's Future provides a generic, type-safe abstraction for asynchronous results in the go-pantheon framework, implemented in xsync/future.go.
  • The implementation uses a sync.RWMutex and a close-only-once done channel for efficient thread synchronization and lock-free completion signaling.
  • Completion methods like Complete() and Cancel() offer idempotent state transitions with deterministic error propagation via ErrFutureCancelled.
  • Retrieval methods including Get(), GetWithContext(), and GetWithTimeout() support various waiting strategies from blocking to cancellation-aware patterns.
  • The Then function enables composable asynchronous pipelines by chaining futures using the panic-safe Go helper from xsync/routines.go.

Frequently Asked Questions

What is the difference between fabrica-util's Future and Go channels?

While Go channels provide primitive communication mechanisms for passing data between goroutines, fabrica-util's Future offers a higher-level abstraction specifically designed for single-value asynchronous results. The Future encapsulates both value and error states in a thread-safe container, provides idiomatic timeout and cancellation support through context integration in GetWithContext, and enables functional composition via the Then method. These features require significant boilerplate when implemented with raw channels alone, where developers must manually manage separate error channels or timeout select statements.

How does fabrica-util handle panics in asynchronous Future chains?

The framework uses the Go helper function defined in xsync/routines.go (lines 48-85) to execute all asynchronous callbacks, including those in Then chains. This helper automatically recovers from panics using recover(), logs detailed error information with structured context, and prevents goroutine crashes from propagating to the top of the stack. By leveraging Go for all background execution, Future pipelines remain robust even when individual transformation functions panic unexpectedly.

Can a fabrica-util Future be completed multiple times?

No, completion is strictly idempotent. Once Complete() or Cancel() is called on a Future instance, subsequent invocations are silently ignored by the implementation in xsync/future.go (lines 32-46). This safety mechanism uses the internal mutex to check completion status before writing, ensuring that producers cannot accidentally overwrite results after consumers have already observed the final state. This prevents race conditions in complex workflows where multiple goroutines might attempt to finalize the same computation.

How does GetWithContext differ from standard channel select statements?

GetWithContext combines the efficiency of the Future's internal done channel with standard context cancellation patterns in a single method call. Unlike manual select statements that require boilerplate to handle both channel receive and context done cases simultaneously, GetWithContext automatically prioritizes the result if already available while respecting cancellation signals. The method returns ctx.Err() only if the context expires before the Future completes, otherwise it returns the stored value and error, eliminating the need for callers to manage complex select logic and priority switching.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →