ruby-concurrency/concurrent-ruby · error · Concurrent::MultipleAssignmentError
Concurrent::MultipleAssignmentError
Error message
Concurrent::MultipleAssignmentError
What it means
An IVar is a write-once container. IVar#set first flips state from :pending to :processing with an atomic compare-and-set (ivar.rb:115); if the IVar is already fulfilled or rejected, or another writer currently holds :processing, the CAS fails and Concurrent::MultipleAssignmentError (errors.rb:33) is raised. The library enforces single assignment instead of silently overwriting the value.
Source
Thrown at lib/concurrent-ruby/concurrent/ivar.rb:115
observer.send(func, Time.now, self.value, reason) if direct_notification
observer
end
# @!macro ivar_set_method
# Set the `IVar` to a value and wake or notify all threads waiting on it.
#
# @!macro ivar_set_parameters_and_exceptions
# @param [Object] value the value to store in the `IVar`
# @yield A block operation to use for setting the value
# @raise [ArgumentError] if both a value and a block are given
# @raise [Concurrent::MultipleAssignmentError] if the `IVar` has already
# been set or otherwise completed
#
# @return [IVar] self
def set(value = NULL)
check_for_block_or_value!(block_given?, value)
raise MultipleAssignmentError unless compare_and_set_state(:processing, :pending)
begin
value = yield if block_given?
complete_without_notification(true, value, nil)
rescue => ex
complete_without_notification(false, nil, ex)
end
notify_observers(self.value, reason)
self
end
# @!macro ivar_fail_method
# Set the `IVar` to failed due to some error and wake or notify all threads waiting on it.
#
# @param [Object] reason for the failure
# @raise [Concurrent::MultipleAssignmentError] if the `IVar` has already
# been set or otherwise completedView on GitHub (pinned to 0b88d5ff75)
Solutions
- Use ivar.try_set(value) (ivar.rb:145): it swallows MultipleAssignmentError and returns true/false, which is exactly the race-safe guard for competing writers.
- Check ivar.incomplete? before #set to catch the common sequential double-set.
- Restructure so exactly one completion path exists (cancel the timeout when the response wins).
- Rescue Concurrent::MultipleAssignmentError when losing a race is expected and the first result should stand.
Example fix
# before ivar.set(response) # raises MultipleAssignmentError if timeout path completed first # after ivar.try_set(response) # returns false when already completed; no exception
Defensive patterns
Strategy: validation
Validate before calling
ivar.set(response) if ivar.incomplete? # best-effort sequential guard # for real multi-writer races use the built-in race-safe form: ivar.try_set(response)
Try / catch
begin ivar.set(value) rescue Concurrent::MultipleAssignmentError # first writer won; keep its value end
Prevention
- Prefer try_set whenever more than one path can complete the IVar.
- Give each IVar a single owner; make timeout and response paths mutually exclusive.
- Treat MultipleAssignmentError in sequential code as a logic bug, not a retryable failure.
When it happens
Trigger: Calling ivar.set(1) twice; calling #set after #fail; two threads racing to set the same IVar where the loser raises; setting an IVar that a framework component (dataflow graph, actor handler, promise adapter) already completed.
Common situations: Request/response handoffs where both a timeout timer and the response path try to complete the same IVar; worker fan-in where several workers write one result slot; retry logic that re-sets the IVar after a failed attempt.
Related errors
- must set with either a value or a block
- provide only a value or a block
- cannot provide both an observer and a block
- no block given
- Not all dependencies are IVars. Dependencies: #{ inputs.insp
AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21).
Data as JSON: /api/errors/be1e0473dd6886f4.
Report an issue: GitHub.