ruby-concurrency/concurrent-ruby · error · Concurrent::IllegalOperationError
Recursive call to #value during evaluation of the Delay
Error message
Recursive call to #value during evaluation of the Delay
What it means
Concurrent::Delay runs its block exactly once, on the first call to #value (or #deref/#wait). Under the object's re-entrant monitor the evaluating thread sets @evaluation_started (delay.rb:84); a second call to #value made from inside the Delay's own block on the same thread reaches the elsif at delay.rb:91 while the state is still incomplete, and the library raises Concurrent::IllegalOperationError instead of deadlocking the monitor or returning a half-computed value.
Source
Thrown at lib/concurrent-ruby/concurrent/delay.rb:92
# @return [Object] the current value of the object
#
# @!macro delay_note_regarding_blocking
def value(timeout = nil)
if @executor # TODO (pitr 12-Sep-2015): broken unsafe read?
super
else
# this function has been optimized for performance and
# should not be modified without running new benchmarks
synchronize do
execute = @evaluation_started = true unless @evaluation_started
if execute
begin
set_state(true, @task.call, nil)
rescue => ex
set_state(false, nil, ex)
end
elsif incomplete?
raise IllegalOperationError, 'Recursive call to #value during evaluation of the Delay'
end
end
if @do_nothing_on_deref
@value
else
apply_deref_options(@value)
end
end
end
# Return the value this object represents after applying the options
# specified by the `#set_deref_options` method. If the delayed operation
# raised an exception, this method will raise that exception (even when)
# the operation has already been executed).
#
# @param [Numeric] timeout the maximum number of seconds to wait
# @return [Object] the current value of the object
# @raise [Exception] when `#rejected?` raises `#reason`View on GitHub (pinned to 0b88d5ff75)
Solutions
- Remove the self-reference: compute the recursive step with plain arguments/methods and let the Delay block only orchestrate the top level.
- Replace recursion with iteration inside the block (accumulate into a local variable).
- Model multi-step dependencies with Concurrent::Promises future chains (then/flat), which compose acyclic dependencies cleanly.
- If re-entry is possible but unexpected, rescue Concurrent::IllegalOperationError at the call site and surface a domain-specific cycle error.
Example fix
# before
fact = Concurrent::Delay.new { fact.value * 1 } # re-enters itself on first #value
# after
def compute(n) = n.zero? ? 1 : n * compute(n - 1)
fact = Concurrent::Delay.new { compute(5) } Defensive patterns
Strategy: try-catch
Try / catch
begin
delay.value
rescue Concurrent::IllegalOperationError => e
raise DependencyCycleError, "Delay block re-enters the Delay: #{e.message}"
end Prevention
- Never reference the Delay object inside its own block; pass inputs as arguments.
- Keep recursive algorithms out of Delay blocks; make them iterative or plain methods.
- Audit dependency graphs for cycles before wiring Delays together.
- Prefer Concurrent::Promises chains (then/flat) for multi-step lazy dependencies.
When it happens
Trigger: The Delay's block dereferences the same Delay: d = Concurrent::Delay.new { d.value }; a memoization cache where computing key A re-enters cache[A].value; two lazily-initialized resources whose blocks dereference each other; a callback invoked synchronously inside the block calls delay.value again.
Common situations: Converting recursive algorithms (tree walks, fibonacci, dependency resolution) to lazy memoized form with Delay; building dependency graphs that accidentally contain a cycle; refactors that pass the Delay instance into its own block; self-referential singleton initialization.
Related errors
- no block given
- Not all dependencies are IVars. Dependencies: #{ inputs.insp
- no block given
- number of threads must be greater than zero
- no block given
AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21).
Data as JSON: /api/errors/bfb4e350faf4b232.
Report an issue: GitHub.