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_state

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Use then for plain transformations; reserve flat for tasks whose block returns a Future.
  2. Match the level to real nesting: flat(1) (the default) for a single layer.
  3. 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

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.