In Ruby with concurrent-ruby, how do Concurrent::Promises.future, zip and then run tasks concurrently and combine results, and what does value return on failure?
answer
- future starts at once on :io
- zip waits for all, yields splatted values
- then runs only after fulfilment
- value gives nil when rejected
- value! raises the reason
basics
~20 sConcurrent::Promises.future { ... } starts a task immediately on the global :io pool; Promises.zip(a, b) waits for all and then { |x, y| ... } receives the values. On rejection value returns nil silently, while value! raises the error.
solid answer
~40 s`Concurrent::Promises.future(*args) { |*args| ... }` returns a `Future` and starts the block right away on the default executor, the global `:io` pool (`future_on(:fast, ...)` picks another). `Concurrent::Promises.zip(f1, f2)` (alias of `zip_futures`, also `f1 & f2`) resolves once every input has resolved; it is fulfilled with an Array of values only if all were fulfilled. `then { |a, b| ... }` runs after fulfilment and receives a zipped Array splatted into arguments; if its block raises, the new future is rejected. `rescue { |reason| ... }` handles rejection. The trap is reading results: `value` blocks and returns `nil` when the future was rejected (and on timeout), while `value!` raises the reason, so a failed lookup silently becomes `nil` if you use `value`.
code
ruby · 8 linesrequire "concurrent"
f = Concurrent::Promises.future { raise ArgumentError, "bad token" }
f.value # => nil (rejected, error hidden)
f.rejected? # => true
f.reason # => #<ArgumentError: bad token>
f.value! # raises ArgumentError: bad tokengo deeper
Recall that Promises.future starts work in the background and value waits for it; remember that value returns nil when the task failed.
Explain zip waiting for all inputs, then receiving splatted values, rescue for rejection, and why value! is the safe way to read a result.
Build a fan-out with a bounded executor through future_on, failures surfaced with value! or rescue, and timeouts checked explicitly, since a timed-out value! returns nil.
Decide where futures belong in a codebase versus background jobs, and set conventions for executors, timeouts and error propagation.
## The shape of the API `Concurrent::Promises` is a module of factory methods. You never call `Future.new`; you call: - **`Concurrent::Promises.future(*args) { |*args| ... }`**: creates a `Future` and starts evaluating the block immediately on the default executor, `:io`, which is the gem's global cached thread pool. - **`Concurrent::Promises.future_on(executor, *args) { ... }`**: the same on a chosen executor: `:io`, `:fast`, `:immediate` or your own pool. - **`Concurrent::Promises.zip(*futures)`**: an alias of `zip_futures`; also written `f1 & f2`. A `Future` is **pending**, then **fulfilled** with a value or **rejected** with a reason (usually an exception). ## Fanning out the lookups for a push notification Before sending a notification, a service needs the user's device tokens and their notification preferences, which come from two slow calls: ```ruby require "concurrent" tokens = Concurrent::Promises.future(user_id) { |id| DeviceTokens.for(id) } prefs = Concurrent::Promises.future(user_id) { |id| Preferences.for(id) } delivery = Concurrent::Promises.zip(tokens, prefs).then do |list, pref| pref.muted? ? [] : list.map { |t| PushClient.deliver(t, message) } end delivery.value!(5) ``` Both lookups run concurrently on the `:io` pool. `zip` produces a future that resolves when both inputs have resolved, and `then` receives the zipped values **splatted** into separate block arguments, `list` and `pref`. ## How each combinator behaves | Method | Runs its block when | Result | |---|---|---| | `future.then { \|v\| ... }` | the future is fulfilled | new future with the block's value, or rejected if the block raises | | `future.rescue { \|reason\| ... }` | the future is rejected | new future fulfilled with the block's value | | `Promises.zip(a, b)` | all inputs have resolved | fulfilled with `[va, vb]` only if every input was fulfilled | | `Promises.any(a, b)` | the first input resolves | that input's result | `then` skips its block when the source was rejected, but the new future still resolves, rejected with the same reason, so the failure travels down the chain until a `rescue` or a `value!` meets it. A zipped future is **not** fail-fast: it waits for every input, and if some were rejected its reason is an Array of reasons with `nil` in the fulfilled positions. ## Reading the result: value vs value! This is the part interviewers probe: 1. **`value(timeout = nil, timeout_value = nil)`** blocks until resolved and returns the value if fulfilled, **`nil` if rejected**, and `timeout_value` (default `nil`) if the timeout passes. The exception is not raised. 2. **`value!(timeout = nil, timeout_value = nil)`** does the same but **raises** the rejection reason. For a zip with several failures, it raises `Concurrent::MultipleErrors`. 3. **`reason`** returns the exception of a rejected future and `nil` for a fulfilled one; **`result`** returns the triplet `[fulfilled?, value, reason]`. 4. **`wait`** only blocks; `fulfilled?` and `rejected?` check the state afterwards. So `delivery.value` on a failed lookup returns `nil`, and code that does `delivery.value.each` then raises a confusing `NoMethodError` far from the real cause, or, worse, treats `nil` as "nothing to send". Use `value!` or check `rejected?` and `reason`. ## Choosing the executor Every factory method has an `_on` variant that takes an executor. The three symbols the gem understands: | Symbol | Resolves to | Suited for | |---|---|---| | `:io` (default) | `Concurrent.global_io_executor`, a `CachedThreadPool` | blocking work: HTTP calls, database queries | | `:fast` | `Concurrent.global_fast_executor`, a `FixedThreadPool` of at least 2 threads, sized from the processor count | short CPU-bound callbacks that never block | | `:immediate` | `Concurrent.global_immediate_executor` | runs on the calling thread; meant for tests and debugging | You can also pass any executor object, such as your own `FixedThreadPool`. `then` and `rescue` run on the future's default executor unless you use `then_on` or `rescue_on`. ## Practical rules - Start futures for independent work, then combine with `zip` instead of calling `value` on each in turn. - Keep blocks free of shared mutable state; pass inputs as arguments and return results. - Always read the end of a chain with `value!` or handle it with `rescue`; with a timeout, remember that `value!(5)` returns its `timeout_value` (default `nil`) rather than raising when time runs out, so check `resolved?` afterwards. - Remember that the `:io` pool grows threads on demand; for heavy fan-out, pass your own bounded pool with `future_on`.
- One of three zipped futures is rejected after 10 ms while the others take 2 seconds. When does the zipped future resolve, and with what?`Promises.zip` waits until every input has resolved, so it resolves after about 2 seconds, not 10 ms. Because one input was rejected, the zipped future is rejected; its reason is an Array of reasons with `nil` for the fulfilled inputs, and `value!` raises that single error. Use `Promises.any` or per-future `rescue` if you need to react to the first failure.
- What is the difference between `then` and `rescue` on a `Concurrent::Promises` future?`then` runs its block only when the future is fulfilled, passing the value; `rescue` runs only when it is rejected, passing the reason. Each returns a new future. A `then` on a rejected future skips its block and yields a future rejected with the same reason; a `rescue` on a fulfilled future passes the value through unchanged.
- Why might you pass your own executor with `future_on` instead of using `future`?`future` uses the global `:io` executor, a `CachedThreadPool` that creates a new thread whenever no idle one is available. Fanning out thousands of futures can therefore start thousands of threads. `future_on(pool, ...)` with a bounded `FixedThreadPool` caps the concurrency and keeps unrelated work from competing for the global pool.
saying these in an interview costs you the question
- Expects future.value to raise the exception of a rejected future
- Thinks Promises.zip rejects as soon as the first input fails
- Believes Promises.future waits until value is called to start
- Receives a zipped Array as one block argument instead of splatted values
- Wraps a rescue around the future call site to catch errors from its block