temporalio/temporal · error
future has already been completed
Error message
future has already been completed
What it means
A Future can only be completed once; Set uses an atomic CAS from pending to setting, and if the future is not pending (already completed or concurrently being completed), it panics. This enforces the single-completion contract that callers of Get/Ready rely on.
Source
Thrown at common/future/future_impl.go:77
if f.Ready() {
return f.value, f.err
}
var value T
return value, errorFutureNotReady
}
func (f *FutureImpl[T]) Set(
value T,
err error,
) {
// cannot directly set status to `ready`, to prevent data race in case multiple `Get` occurs
// instead set status to `setting` to prevent concurrent completion of this future
if !atomic.CompareAndSwapInt32(
&f.status,
pending,
setting,
) {
panic("future has already been completed")
}
f.value = value
f.err = err
atomic.CompareAndSwapInt32(&f.status, setting, ready)
close(f.readyCh)
}
// Sets the value of the future, if it has not been set already. Returns true if this call set the value.
func (f *FutureImpl[T]) SetIfNotReady(
value T,
err error,
) bool {
if !atomic.CompareAndSwapInt32(
&f.status,
pending,
setting,
) {View on GitHub (pinned to bde624efd1)
Solutions
- Track completion with a sync.Once or the future's own state before calling Set
- Ensure only one goroutine/path owns completion; others should wait via Get/Ready
- If double completion is possible, use a wrapper that swallows the second Set instead of a raw FutureImpl
Example fix
// before
if err != nil {
f.Set(nil, err)
}
f.Set(result, nil) // panics if err branch ran
// after
if err != nil {
f.Set(nil, err)
return
}
f.Set(result, nil) Defensive patterns
Strategy: try-catch
Try / catch
defer func(){ recover() }(); f.Set(v, err) Prevention
- sync.Once around completion
- Single completion owner
When it happens
Trigger: Calling Set (or SetError, or any completion method) twice on the same FutureImpl; racing two goroutines that both attempt to resolve the future; a library internally completing a future while user code also completes it.
Common situations: Retry logic that resolves the future on each attempt instead of only the first success; broadcast patterns where multiple watchers write the result; callback invoked twice by an upstream library.
Related errors
- HistoryEventIterator Next() should return either a history e
- Current cluster name is empty
- Version increment <= 0 or > 2147483647
- <dynamic updateVersionToClusterName err>
- Current cluster is not specified in cluster info
AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01).
Data as JSON: /api/errors/51f9584e135dd293.
Report an issue: GitHub.