In Dart, what do `Future.timeout` and `Future.any` actually do to the futures they give up on, and why doesn't either stop the slow work?
answer
- a new future, not a cancellation
- TimeoutException unless onTimeout given
- late results and errors are ignored
- any: first completion wins, even an error
- stop work through the source's own API
basics
~20 sFuture.timeout returns a new future that fails with TimeoutException, or uses onTimeout, if the source is late; Future.any completes with the first result, value or error. Neither cancels anything: the losing work keeps running and its result is discarded.
solid answer
~40 s`future.timeout(limit)` returns a **new** future. If the source completes within `limit`, the new one completes the same way; otherwise it completes with a `TimeoutException`, or with the result of `onTimeout` if you pass one. The source is not stopped: it keeps running, and a value or even an error it produces after the limit is simply ignored. `Future.any(futures)` completes with the result of the **first** future to complete, whether that is a value or an error, and discards the rest; given an empty list it never completes. Because a Dart `Future` is only a handle to a result, not to the work, neither method can cancel anything. To really stop a slow request you use the producer's own mechanism, such as closing its client, cancelling its subscription or checking a flag.
go deeper
Know that timeout throws a TimeoutException unless you give onTimeout, and that Future.any returns whichever future finishes first.
Explain that both return new futures, that the source keeps running, and that any forwards the first completion even when it is an error.
Spot retry-on-timeout loops that pile up in-flight work, and cancel through the producer's API rather than trusting the future to stop it.
Define timeout and cancellation policy for the app's data layer, so every slow dependency has both a user-visible fallback and a real way to stop.
## A future is a result handle, not a task handle A Dart `Future<T>` represents the **eventual result** of some work. It has no method to stop that work, and it knows nothing about the socket, timer or file behind it. Both `timeout` and `Future.any` are built on that fact: they create a new future and decide which result to forward to it. The original work goes on regardless. ## `Future.timeout` The signature is `Future<T> timeout(Duration timeLimit, {FutureOr<T> onTimeout()?})`. Its behaviour, as documented in `dart:async`: 1. If the **source future** completes before `timeLimit`, the returned future completes with the same value or error. 2. If it does not, and `onTimeout` is **omitted**, the returned future completes with a **`TimeoutException`**. 3. If `onTimeout` is **given**, its result is used instead: a value, a thrown error, or a `Future<T>` whose eventual result is used even if the source finishes first in the meantime. 4. The source future **can still complete later**. Its value is not used, and an error it produces after the limit is **ignored** as well. ```dart final stats = await loadStats().timeout( const Duration(seconds: 3), onTimeout: () => Stats.empty(), // the tile shows an empty state ); ``` For a dashboard, this lets one slow tile degrade to a placeholder while the other tiles render, but the HTTP request behind `loadStats` is still in flight and still consumes the connection. ## `Future.any` `static Future<T> any<T>(Iterable<Future<T>> futures)` completes with the result of the **first future to complete**: - A **value** from the fastest future wins. - An **error** from the fastest future also wins; `any` does not skip failures to wait for a success. - The results of all the other futures are **discarded**, but those futures keep running. - With an **empty** iterable, or if none completes, the returned future **never completes**. That makes `any` suitable for racing equivalent sources, such as two mirrors or a cache against the network, only if a fast failure is acceptable as the answer. To mean "first success", you must turn failures into never-completing or sentinel results yourself. ## Comparison | | `timeout` | `Future.any` | |---|---|---| | Input | one source future | many futures | | Completes with | source result, `TimeoutException` or `onTimeout` | first result, value or error | | Losers | source keeps running, late result ignored | all others keep running, results discarded | | Cancels work | no | no | ## Choosing where the timeout sits On the dashboard, *where* you apply `timeout` decides what the user sees: - **Around the whole group**, `(loadProfile(), loadFeed(), loadStats()).wait.timeout(...)`, one slow tile fails the entire screen, even though two tiles already had data. - **Around each call**, `loadStats().timeout(..., onTimeout: () => Stats.empty())`, a slow tile degrades on its own while the others render. - **Both**: per-call limits for graceful degradation, plus a longer overall limit as a safety net. Whichever you choose, remember that every abandoned call is still running, so a per-tile timeout paired with a refresh button can multiply in-flight requests unless the refresh cancels the previous attempt. ## Actually stopping the work Because the future cannot cancel its producer, cancellation has to go through whatever started the work: - close or abort the client that issued the request; - cancel the `StreamSubscription` the value comes from; - check a cancellation flag between steps of your own long operation; - for CPU-bound work in another isolate, terminate that isolate. A common senior-level bug is a retry loop that wraps a call in `timeout`, retries on `TimeoutException`, and so piles up duplicate in-flight requests because none of the timed-out attempts ever stopped. ## What interviewers want to hear - `timeout` changes **when you stop waiting**, not **whether the work runs**. - `any` forwards the first **completion**, which may be an error. - Errors from abandoned futures after a timeout are ignored by the timeout future, so they do not reach your `catch`. - Real cancellation needs an API designed for it.
- With `onTimeout` returning a future, which result wins if the source finishes while that fallback is still running?The fallback. Once the time limit passes and `onTimeout` runs, the returned future is completed from `onTimeout`'s result, even if the source future completes before the fallback does. It only matters that the source did not complete in time.
- How would you race two mirrors with Future.any but accept only the first success?Map each future so a failure never wins: for example, turn an error into a future that never completes, or into a sentinel you filter out, and add a final timeout so an all-failure case still ends. Plain `Future.any` would otherwise forward whichever mirror fails fastest.
saying these in an interview costs you the question
- timeout cancels the underlying request when the limit passes
- Future.any skips errors until one future succeeds
- A late error from the source still reaches the timeout future's catch
- Future.any with an empty list completes immediately with null
- Retrying on TimeoutException leaves no extra requests running