skip to content

In Dart, a dashboard loads profile, feed and stats from three independent futures; how do you run them concurrently and collect typed results?

level: middleimportance: must knowfreq 68%

answer

  1. sequential awaits add up latencies
  2. start all, then wait once
  3. Future.wait: eagerError defaults to false
  4. record .wait keeps each type
  5. ParallelWaitError holds values and errors

basics

~20 s

Start all three futures before awaiting any: Future.wait([a, b, c]) gives a List in argument order, and Dart 3's (a, b, c).wait gives a typed record. Awaiting one after another serialises the calls and sums their latencies.

solid answer

~40 s

Writing `await loadProfile(); await loadFeed(); await loadStats();` starts each call only after the previous one finishes, so the dashboard waits for the **sum** of three latencies. Because a Dart future starts when it is created, calling all three first and awaiting them together costs roughly the **slowest** one. `Future.wait([a, b, c])` returns the results as a `List` in argument order; with the default `eagerError: false` it waits for every future and then completes with the first error that occurred, dropping later ones, and an optional `cleanUp` disposes successful results after a failure. For mixed types, Dart 3's record extension is better: `final (profile, feed, stats) = await (loadProfile(), loadFeed(), loadStats()).wait;` keeps each type, and on failure throws a `ParallelWaitError` whose `values` and `errors` records show exactly which calls succeeded.

code

dart · 11 lines
dart
Future<void> loadDashboard() async {
  try {
    final (profile, feed, stats) =
        await (loadProfile(), loadFeed(), loadStats()).wait;
    render(profile, feed, stats);
  } on ParallelWaitError<(Profile?, List<Post>?, Stats?),
      (AsyncError?, AsyncError?, AsyncError?)> catch (e) {
    final (profile, feed, stats) = e.values;
    renderPartial(profile, feed, stats, e.errors);
  }
}

go deeper

for a junior

Know that independent calls should be started together and awaited with Future.wait, which returns results in the order you passed them.

for a middle

Explain eagerError's default, what error Future.wait reports, and why a Dart 3 record with .wait keeps each result's type.

for a senior

Design partial-failure handling with ParallelWaitError, avoid unhandled errors from futures started but awaited late, and use cleanUp for resources.

for a principal

Decide per screen whether a failed dependency blocks everything or degrades one tile, and make that policy consistent across the app's data loading.

## The sequential trap A dashboard that shows a profile card, a feed and a stats tile usually needs three independent network calls. The naive version awaits them one by one: ```dart final profile = await loadProfile(); // ~300 ms final feed = await loadFeed(); // ~500 ms, starts only now final stats = await loadStats(); // ~200 ms, starts only now // total ~1000 ms ``` Each call is **started** only when the previous `await` has finished, so the latencies add up. Nothing is wrong with `await` itself; the calls were simply issued in sequence. ## Start first, wait once In Dart, calling an `async` function starts its work immediately and returns a future. So the fix is to **create all the futures before awaiting any of them**, then wait for the group. The total becomes roughly the slowest call (~500 ms above). This is **concurrency on one isolate**: the three requests are in flight at the same time and their I/O overlaps, but Dart code still runs one piece at a time. CPU-heavy work does not speed up this way; that needs another isolate. ## `Future.wait` `Future.wait<T>(Iterable<Future<T>> futures, {bool eagerError = false, void cleanUp(T successValue)?})` returns `Future<List<T>>`: - **Order**: results are in the order the futures were supplied, not the order they completed. - **Default error behaviour** (`eagerError: false`): it waits for **all** futures, then completes with the **first error that occurred**; later errors are silently dropped. - **`eagerError: true`**: completes with the first error immediately, without waiting for the rest (which keep running). - **`cleanUp`**: after an error, called for each non-null successful value, so resources such as open handles are not lost. - **Types**: all futures share one `T`, so a profile, a feed and a stats object collapse to `List<Object>` and need casts. ## Records and `.wait` (Dart 3.0) Dart 3.0 added `wait` getters on records of two to nine futures, and on any `Iterable<Future<T>>`: ```dart final (profile, feed, stats) = await (loadProfile(), loadFeed(), loadStats()).wait; ``` - Each field keeps its own static type: `profile` is a `Profile`, `feed` a `List<Post>`. - The returned future completes only when **all** futures have completed. - If any fail, it completes with a **`ParallelWaitError`**. Its `values` has the same shape as the input with `null` for failed slots, and its `errors` holds an `AsyncError` for each failed slot and `null` for successes. The dashboard can render the tiles that loaded and an error state for the rest. | | `Future.wait` | record `.wait` | |---|---|---| | Result type | `List<T>` | typed record | | Mixed value types | collapse to a common supertype | preserved per field | | On error | first error only | `ParallelWaitError` with every value and error | | Early failure option | `eagerError: true` | always waits for all | | Cleanup hook | `cleanUp` | inspect `values` yourself | ## A variable number of tiles When the dashboard's tiles come from configuration rather than a fixed three, the `Iterable` extension does the same job: `final tiles = await [for (final id in tileIds) loadTile(id)].wait;` returns a `List` in input order. On failure it throws a `ParallelWaitError` whose `values` is a list with `null` in failed slots and whose `errors` is a list of the same length holding an `AsyncError` for each failure, so the screen can still render every tile that loaded. ## Why not await started futures one by one? A tempting middle ground is to start all three, then `await a; await b; await c;`. It is concurrent, but it has an error-handling hole. The SDK documents that a future's error is reported as **unhandled** if it completes with an error before any handler is attached. If `stats` fails while the code is still awaiting `profile`, that error is reported as unhandled even though a later `await c` would also see it. `Future.wait` and `.wait` attach handlers to every future at once, which avoids this. ## Checklist 1. Identify which calls are truly independent. 2. Start them together and wait once, preferring the record `.wait` for mixed types. 3. Decide whether a partial failure should fail the screen or degrade a single tile, and use `ParallelWaitError` accordingly.

  • What does `eagerError: true` change in `Future.wait`, and does it stop the other futures?
    With `eagerError: true` the returned future completes with the first error as soon as it happens instead of waiting for all futures. It does not stop the others: they keep running, their results are simply not delivered through this call. With the default `false`, `Future.wait` waits for every future and then reports the first error.
  • What is the `cleanUp` parameter of `Future.wait` for?
    When at least one future fails, the returned future carries only the error, so successful results would be lost. `cleanUp` is called with each non-null successful value in that case, letting you close a file, release a handle or cancel a subscription the successful call opened. It is unused when nothing fails, and it should not throw.

Ordering three dishes at a counter: asking for the second only after the first arrives means waiting for all three cooking times back to back. Placing all three orders at once and collecting them when the last is ready costs only the slowest dish.

saying these in an interview costs you the question

  • Sequential awaits already run the calls in parallel
  • Future.wait returns results in completion order
  • Future.wait fails immediately on the first error by default
  • Future.wait runs each future on its own thread
  • eagerError: true cancels the remaining futures