skip to content

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?

level: seniorimportance: should knowfreq 40%

answer

  1. a new future, not a cancellation
  2. TimeoutException unless onTimeout given
  3. late results and errors are ignored
  4. any: first completion wins, even an error
  5. stop work through the source's own API

basics

~20 s

Future.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

for a junior

Know that timeout throws a TimeoutException unless you give onTimeout, and that Future.any returns whichever future finishes first.

for a middle

Explain that both return new futures, that the source keeps running, and that any forwards the first completion even when it is an error.

for a senior

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.

for a principal

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