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
- Call set on the root promise only: root = Concurrent::Promise.new; root.set(1)
- To supply a child's value, chain it: root.then { |v| 10 } — the callback defines the child's result
- 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
- Model pipelines so values flow from parents to children via then; never inject into children
- Keep a reference to the root promise when a chain needs manual fulfillment
- Use Promise.fulfill(value) / Promise.reject(reason) for pre-computed roots
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
- rescuers and block are both missing
- no block given
- it is not rejected
- agent is not failed
- no block given
AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21).
Data as JSON: /api/errors/3c1e5d6b293dde05.
Report an issue: GitHub.