ruby-concurrency/concurrent-ruby · error · Concurrent::PromiseExecutionError

supported only on root promise

Error message

supported only on root promise

What it means

Concurrent::Promise#set manually fulfills a promise with a value or block, but only a root promise (one created by Promise.new / Promise.fulfill / Promise.reject) owns its result. A child promise produced by then/on_success/rescue derives its value from its parent's callback, so calling set on it raises PromiseExecutionError 'supported only on root promise' (a StandardError subclass defined in concurrent/promise.rb).

Source

Thrown at lib/concurrent-ruby/concurrent/promise.rb:263

    # @return [Promise] a reference to `self`
    def execute
      if root?
        if compare_and_set_state(:pending, :unscheduled)
          set_pending
          realize(@promise_body)
        end
      else
        compare_and_set_state(:pending, :unscheduled)
        @parent.execute
      end
      self
    end

    # @!macro ivar_set_method
    #
    # @raise [Concurrent::PromiseExecutionError] if not the root promise
    def set(value = NULL, &block)
      raise PromiseExecutionError.new('supported only on root promise') unless root?
      check_for_block_or_value!(block_given?, value)
      synchronize do
        if @state != :unscheduled
          raise MultipleAssignmentError
        else
          @promise_body = block || Proc.new { |result| value }
        end
      end
      execute
    end

    # @!macro ivar_fail_method
    #
    # @raise [Concurrent::PromiseExecutionError] if not the root promise
    def fail(reason = StandardError.new)
      set { raise reason }
    end

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Call set on the root promise only: root = Concurrent::Promise.new; root.set(1)
  2. To supply a child's value, chain it: root.then { |v| 10 } — the callback defines the child's result
  3. For an immediately-fulfilled root use Concurrent::Promise.fulfill(value) or Concurrent::Promise.reject(reason)

Example fix

# before
child = promise.then { |v| v * 2 }
child.set(10)

# after
root = Concurrent::Promise.new { 5 }
root.execute
root.then { |v| v * 2 } # child value comes from the parent
Defensive patterns

Strategy: validation

Validate before calling

root = Concurrent::Promise.new(executor: executor)
root.set(value) # only the root may be set
child = root.then { |v| v * 2 }

Type guard

->(promise) { promise.send(:root?) } # true only for promises created by Promise.new/fulfill/reject

Try / catch

begin
  promise.set(value)
rescue Concurrent::PromiseExecutionError => e
  raise PromiseMisuse, "manual set only works on root promises: #{e.message}"
end

Prevention

When it happens

Trigger: p2 = promise.then { |v| v * 2 }; p2.set(10); calling set on a promise obtained from an aggregate (zip/merge) chain; trying to override or 'correct' a downstream promise's value manually.

Common situations: Porting manual-fulfillment code from IVar/Future to Promise chains; attempting to inject a result into a derived step during error recovery; debugging pipelines by forcing values into intermediate nodes.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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