How Promise.SetResult Handles Concurrent Resolution Attempts in Go

Promise.SetResult uses an atomic boolean swap to ensure exactly one goroutine succeeds when multiple threads attempt concurrent resolution, making all subsequent calls no-ops that return false.

The aperturerobotics/util repository provides a thread-safe Promise[T] type for managing one-time asynchronous results. When multiple goroutines race to resolve the same promise, the SetResult method guarantees safe concurrent resolution without locks or panics. This design pattern is critical for coordination primitives where multiple producers might attempt to signal completion.

The Atomic Resolution Mechanism

The concurrency safety of SetResult relies on a single atomic.Bool field named isDone. This flag coordinates the transition from pending to resolved using lock-free atomic operations.

First-Caller-Wins Semantics

Inside promise/promise.go (lines 43-53), the method uses p.isDone.Swap(true) to atomically attempt resolution:

func (p *Promise[T]) SetResult(val T, err error) bool {
    // Atomically swap the flag from false → true.
    // If it was already true, another goroutine set the result first.
    if p.isDone.Swap(true) {
        return false        // resolution already happened
    }
    // First caller proceeds:
    p.result = &val
    p.err = err
    close(p.done)          // unblock any Await callers
    return true
}

The Swap operation returns the previous value. The first goroutine sees false (indicating "not done"), proceeds to store the result and close the channel, then returns true. Every subsequent caller sees true, immediately returns false, and leaves the promise state untouched.

Memory Ordering Guarantees

The atomic swap establishes a happens-before relationship according to the Go memory model. This ensures that writes to p.result, p.err, and the close(p.done) operation are visible to any goroutine that subsequently observes the resolved state through Await or AwaitWithErrCh. Even on weakly-ordered architectures, consumers will see the complete resolution state, not partial writes.

Panic-Free Channel Closing

Because only the winning goroutine reaches the close(p.done) line, the implementation eliminates the risk of double-close panics. The atomic flag acts as a gatekeeper, ensuring the channel is closed exactly once regardless of how many goroutines invoke SetResult simultaneously.

Source Code Analysis

The implementation in promise/promise.go demonstrates deliberate minimalism. By using atomic.Bool rather than a sync.Mutex, the method avoids contention and scheduler overhead during high-concurrency races. The SetResult signature returns a boolean to allow callers to detect whether they won the resolution race, enabling conditional logic for cleanup or logging.

Concurrent Resolution in Practice

The following example demonstrates three workers racing to resolve the same promise. Only one succeeds, and the final result matches the winning worker's value:

func ExampleConcurrentSetResult() {
    p := promise.NewPromise[int]()

    // Worker that tries to set the result after a random delay.
    worker := func(id int, val int) {
        time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
        if ok := p.SetResult(val, nil); ok {
            fmt.Printf("worker %d succeeded\n", id)
        } else {
            fmt.Printf("worker %d lost the race\n", id)
        }
    }

    go worker(1, 42)
    go worker(2, 99)
    go worker(3, 7)

    // Wait for the promise to be resolved.
    result, _ := p.Await(context.Background())
    fmt.Printf("final result: %d\n", result)
}

Typical output shows exactly one success:


worker 2 succeeded
worker 1 lost the race
worker 3 lost the race
final result: 99

Integration with Background Workers

The promise/once.go file (lines 25-70) demonstrates production usage of this safety guarantee. Background goroutines spawned by the Once type call SetResult from multiple concurrent paths, relying on the atomic mechanism to coordinate the single permitted resolution. This pattern appears throughout the aperturerobotics/util codebase wherever lazy initialization or single-flight semantics are required.

Summary

  • Atomic boolean swap in promise/promise.go ensures exactly one goroutine transitions the promise from pending to resolved.
  • Happens-before ordering guarantees that the stored result and error are fully visible to all awaiting consumers.
  • Single channel close prevents panics by ensuring p.done is closed only by the winning caller.
  • Boolean return value allows callers to detect resolution success (true) versus no-op rejection (false).

Frequently Asked Questions

What happens if two goroutines call SetResult at the exact same time?

Only one goroutine succeeds. The first to execute p.isDone.Swap(true) receives false as the previous value, stores the result, and returns true. The second goroutine receives true from the swap, indicating the promise is already resolved, and returns false without modifying state.

Does Promise.SetResult use locks or mutexes?

No, the implementation is lock-free. It uses atomic.Bool operations to coordinate concurrent attempts, avoiding the scheduler overhead and contention associated with sync.Mutex. This makes SetResult suitable for high-frequency concurrent resolution attempts.

Can calling SetResult multiple times cause a panic?

No. The atomic flag prevents the close(p.done) operation from executing more than once. Without this protection, closing an already-closed Go channel would panic, but SetResult safely drops subsequent calls before reaching the channel close.

How does the atomic operation affect memory visibility for other goroutines?

The atomic swap provides a happens-before relationship that synchronizes memory between the resolving goroutine and any goroutine calling Await or AwaitWithErrCh. This ensures that when an awaiting goroutine observes the resolved state, it sees the complete writes to p.result and p.err, not stale or partially updated values.

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 →