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 completed

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. 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.
  2. Check ivar.incomplete? before #set to catch the common sequential double-set.
  3. Restructure so exactly one completion path exists (cancel the timeout when the response wins).
  4. 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

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


AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21). Data as JSON: /api/errors/be1e0473dd6886f4. Report an issue: GitHub.