ruby-concurrency/concurrent-ruby · error · TypeError
returned value #{value.inspect} is not a Future
Error message
returned value #{value.inspect} is not a Future What it means
When a flattened future resolves, FlatFuturePromise#process_on_blocker_resolution (promises.rb:1899-1908) expects the fulfilled value to be another Future/EventFuture while flattening levels remain. If the value is a plain object — because the chain was flattened one level too many, or flat was used where then was meant — the promise evaluates to a TypeError: "returned value <inspect> is not a Future", surfaced by the resulting future (e.g. via value!).
Source
Thrown at lib/concurrent-ruby/concurrent/promises.rb:1907
def process_on_blocker_resolution(future, index)
countdown = super(future, index)
if countdown.nonzero?
internal_state = future.internal_state
unless internal_state.fulfilled?
resolve_with internal_state
return countdown
end
value = internal_state.value
case value
when AbstractEventFuture
add_delayed_of value
value.add_callback_notify_blocked self, nil
countdown
else
evaluate_to(lambda { raise TypeError, "returned value #{value.inspect} is not a Future" })
end
end
countdown
end
end
class RunFuturePromise < AbstractFlatPromise
private
def initialize(delayed, blockers_count, default_executor, run_test)
super delayed, 1, Future.new(self, default_executor)
@RunTest = run_test
end
def process_on_blocker_resolution(future, index)
internal_state = future.internal_stateView on GitHub (pinned to 0b88d5ff75)
Solutions
- Use then for plain transformations; reserve flat for tasks whose block returns a Future.
- Match the level to real nesting: flat(1) (the default) for a single layer.
- Make task returns uniform: wrap plain values with Concurrent::Promises.fulfilled_future(v).
Example fix
# before
Concurrent::Promises.future { heavy(5) }.flat.value!
# -> TypeError: returned value 5 is not a Future
# after
result = Concurrent::Promises.future { Concurrent::Promises.future { heavy(5) } }.flat.value!
# or, for plain transformation:
result = Concurrent::Promises.future { heavy(5) }.then { |v| v }.value! Defensive patterns
Strategy: type-guard
Validate before calling
f.then { |v| v.is_a?(Concurrent::Promises::Future) ? v.flat : v } # flatten only when actually nested Type guard
def future?(value) value.is_a?(Concurrent::Promises::Future) end # inside a task that sometimes returns raw values: result = compute future?(result) ? result : Concurrent::Promises.fulfilled_future(result)
Try / catch
begin
result = f.flat.value!
rescue TypeError => e
raise unless e.message.include?('is not a Future')
result = f.then { |v| v }.value! # fall back to non-flattening chain
end Prevention
- Use then for plain transformations; use flat only when the task returns a Future.
- Keep task return types uniform: always return futures or always return values.
- Wrap stray plain values with Concurrent::Promises.fulfilled_future(v).
- Match the flat level to the actual nesting depth of your chain.
When it happens
Trigger: Concurrent::Promises.future { 5 }.flat.value! — the value 5 is not a Future; flat(2) applied to a future nested only one level; a task that usually returns a future but returns a raw value on some code path.
Common situations: Using flat for ordinary transformations (then is the correct method); pipeline tasks with inconsistent return types (sometimes a future, sometimes a plain value); computed flatten levels exceeding actual nesting.
Related errors
AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21).
Data as JSON: /api/errors/53bdf2f7a740c1d4.
Report an issue: GitHub.