| |

Dart 39 🎯 Parallel Futures Execution using Future.wait and Future.any

A single await expression waits for one future. A sequence of await expressions waits for futures one after another, but the operations themselves run sequentially — the second operation does not start until the first completes. When operations are independent, this sequential waiting is wasteful. Fetching a user profile, an order history, and a list of promotions from three different endpoints takes three times as long as it needs to if the requests are made one at a time. The operations do not depend on each other, so they should run in parallel.

Dart provides two static methods for parallel future handling: Future.wait and Future.any. They answer different questions. Future.wait asks: “When all of these futures complete, what are their values?” It collects the results into a list, preserving the order of the input futures. Future.any asks: “Which of these futures completes first?” It returns the value (or error) of the first future to complete and discards the rest.

The distinction matters because the methods have different failure semantics. Future.wait completes with an error if any of the futures fail, and by default it waits for all futures to complete before reporting the error. Future.any completes with the result of the first future to complete, whether that result is a value or an error. An error in one future can complete the Future.any before the other futures have finished. These behaviors determine which method is appropriate for which scenario.

This chapter covers three areas. First, why parallel future execution exists — the cost of sequential awaits and the situations where operations can and cannot run in parallel. Second, how Future.wait works — collecting results in order, the eagerError and cleanUp parameters, and the error semantics when one or more futures fail. Third, how Future.any works — returning the first completion, discarding the rest, and the practical cases where a race between futures is the right pattern. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing parallel execution and the differences between the two methods.

Key point: Future.wait runs multiple futures in parallel and completes when all of them have completed, returning their values in the order the futures were provided. Future.any runs multiple futures in parallel and completes when the first one completes, returning that future’s value and discarding the rest. Use Future.wait when you need every result; use Future.any when you only need the fastest one .


Why parallel future execution exists

The sequential-await problem. An await expression suspends the function until the future completes. If a function awaits three futures in sequence, the total time is the sum of the three operation times. If each operation takes one second, the function takes three seconds. But if the operations are independent, they could all run at once, and the total time would be the longest operation, not the sum. The sequential form is simple to write, but it serializes work that could be parallel. Future.wait removes the serialization: the futures are created and started before any of them is awaited, so they run concurrently, and the wait is for all of them to finish .

The concurrency model problem. Dart runs on a single thread, so “parallel” does not mean multiple threads executing simultaneously. It means multiple asynchronous operations are in flight at the same time. A network request is handled by the operating system, not by the Dart thread. When three requests are started, the OS handles all three, and the Dart thread is free to process other events. The futures complete as the responses arrive, and the Dart thread processes each completion as an event. The operations overlap in wall-clock time, even though the Dart code runs on one thread. This is concurrency without parallelism, and it is why Future.wait can overlap the waiting for independent I/O operations .

The ordering problem. When multiple futures run in parallel, they complete in whatever order the underlying operations finish. A fast network request might complete before a slow one, regardless of which was started first. Future.wait addresses this by preserving the input order in the result list. The first future’s value is at index 0, the second at index 1, and so on, regardless of completion order. This means the result list has a predictable structure that matches the input list, even though the timing is unpredictable. The caller does not need to know which future finished first; the index is the identity .

The first-result problem. Sometimes only the first result matters. A cache lookup, a primary server, and a fallback server might all be queried, and the fastest response is the one to use. The other responses are irrelevant. Future.any is built for this. It completes as soon as the first future completes, with that future’s value (or error), and ignores the rest. The losing futures continue running in the background, but their results are discarded. This is a race, and the winner is the one that finishes first. It is useful when multiple sources can provide the same data and the fastest one should be used .

The failure-mode problem. Future.wait and Future.any handle errors differently because they answer different questions. Future.wait is about completeness: it needs all results, so an error in any future is a failure of the whole operation. By default, it waits for all futures to complete before reporting the first error, so resources from successful futures can be cleaned up. Future.any is about speed: it needs the first result, so an error in the fastest future is the result. The other futures’ errors are ignored because the race is already over. These semantics follow from the purpose of each method .

The trade-off. Parallel execution is faster, but it is more complex to reason about. The order of completion is not the order of the input list. Errors can arrive from any future at any time. Future.wait with eagerError: false (the default) waits for all futures to complete even when one has already failed, which means the error is not reported until the slowest future finishes. Future.any ignores the results of the losing futures, which is correct for a race but can leave resources unmanaged if the losing futures hold connections or buffers. The trade-off is between the speed of parallel execution and the simplicity of sequential waiting. Use parallel execution when the operations are independent and the speed matters; use sequential awaits when the operations depend on each other or when the simplicity is more important.


a. Future.wait — collecting all results

Future.wait takes an iterable of futures and returns a single future that completes when all of them have completed. The returned future’s value is a list of the results, in the same order as the input iterable .

Future<int> fetchA() async => 1;
Future<int> fetchB() async => 2;
Future<int> fetchC() async => 3;

void main() async {
  final results = await Future.wait([fetchA(), fetchB(), fetchC()]);
  print(results); // [1, 2, 3]
}

The three futures are created before Future.wait is called, which means they all start running immediately. The await waits for all three to complete, and the result is a list with the values in the order the futures were provided. If fetchC completes first, its value is still at index 2 because that is its position in the input list .

The eagerError parameter controls when an error is reported. By default, eagerError is false, which means Future.wait waits for every future to complete before completing with the first error. If eagerError is true, the returned future completes with the error immediately when the first future fails, without waiting for the others .

Future.wait(
  [fetchA(), failingFuture(), fetchC()],
  eagerError: true, // completes immediately on the first error
);

The cleanUp parameter is a function that runs on the successful results when an error occurs. It allows resources from successful futures to be released, since the error path does not provide access to those values .

Future.wait(
  [fetchResource(), fetchResource2()],
  cleanUp: (result) => result.close(), // close resources on error
);

If any future in the list completes with an error, the returned future completes with that error. If multiple futures complete with errors, the first error is used and the others are discarded . An empty iterable returns a future that completes with an empty list.

b. Future.any — returning the first result

Future.any takes an iterable of futures and returns a single future that completes as soon as the first future in the iterable completes. The returned future’s value is the value of that first future. The results of all other futures are discarded .

Future<int> fast() async {
  await Future.delayed(Duration(seconds: 1));
  return 1;
}

Future<int> slow() async {
  await Future.delayed(Duration(seconds: 3));
  return 2;
}

void main() async {
  final result = await Future.any([slow(), fast()]);
  print(result); // 1
}

The slow() future takes three seconds, and the fast() future takes one. Future.any returns the result of fast() after one second. The slow() future continues running in the background, but its result is never used .

An error in the first future to complete also completes Future.any with that error. The method does not distinguish between success and failure; it only cares about which future completes first .

Future<int> failingFast() async {
  await Future.delayed(Duration(seconds: 1));
  throw Exception('fast failure');
}

Future<int> slowSuccess() async {
  await Future.delayed(Duration(seconds: 3));
  return 42;
}

void main() async {
  try {
    await Future.any([slowSuccess(), failingFast()]);
  } catch (e) {
    print(e); // Exception: fast failure
  }
}

If the iterable is empty, or if none of the futures complete, the returned future never completes. This is a silent hang, and it is the caller’s responsibility to ensure the iterable is not empty .

c. When to use each

The choice between Future.wait and Future.any depends on what the caller needs from the parallel operations.

Use Future.wait when every result is needed. A dashboard that displays user profile, order history, and promotions needs all three data sets before it can render. A batch operation that processes a list of items and collects the outcomes needs every outcome. A parallel download that assembles a file from multiple chunks needs every chunk. In these cases, the operation is incomplete until all futures have completed, and the results are combined into a single output .

Use Future.any when only the first result matters. A cache lookup that falls back to a network request only needs the first successful response. A health check that pings multiple servers only needs one server to respond. A race between two algorithms that solve the same problem only needs the winner. In these cases, the other futures are redundant once the first one completes, and their results are discarded .

There is also a middle case: running independent operations in parallel and handling each result as it arrives. Neither Future.wait nor Future.any covers this. The pattern is to attach then callbacks to each future individually, so each completion is processed without waiting for the others. This is useful for streaming results to a UI, where each piece of data can be displayed as it arrives.


Complete Example Session

// ============================================
// PART 1: SEQUENTIAL AWAITS — THE SLOW WAY
// ============================================

// Three independent operations awaited one after
// another. The total time is the sum of the
// operation times.

Future<String> fetchUser() async {
  await Future.delayed(Duration(seconds: 1));
  return 'User';
}

Future<String> fetchOrders() async {
  await Future.delayed(Duration(seconds: 1));
  return 'Orders';
}

Future<String> fetchPromos() async {
  await Future.delayed(Duration(seconds: 1));
  return 'Promos';
}

Future<void> sequential() async {
  final stopwatch = Stopwatch()..start();

  final user = await fetchUser();     // 1 second
  final orders = await fetchOrders(); // 1 second
  final promos = await fetchPromos(); // 1 second

  stopwatch.stop();
  print('Sequential: ${stopwatch.elapsedMilliseconds}ms'); // ~3000ms
}


// ============================================
// PART 2: PARALLEL AWAITS — THE FAST WAY
// ============================================

// The futures are created before the await.
// They run concurrently. The total time is the
// longest operation, not the sum.

Future<void> parallel() async {
  final stopwatch = Stopwatch()..start();

  final results = await Future.wait([
    fetchUser(),   // starts immediately
    fetchOrders(), // starts immediately
    fetchPromos(), // starts immediately
  ]);

  stopwatch.stop();
  print('Parallel: ${stopwatch.elapsedMilliseconds}ms'); // ~1000ms
  print(results); // [User, Orders, Promos]
}


// ============================================
// PART 3: ORDER IS PRESERVED
// ============================================

// The results are in the order of the input
// list, regardless of completion order.

Future<String> fast() async {
  await Future.delayed(Duration(milliseconds: 100));
  return 'fast';
}

Future<String> slow() async {
  await Future.delayed(Duration(seconds: 1));
  return 'slow';
}

Future<void> orderPreserved() async {
  final results = await Future.wait([slow(), fast()]);
  // 'slow' is first in the input, so it is first in the output
  print(results); // [slow, fast]
}


// ============================================
// PART 4: ERROR HANDLING WITH FUTURE.WAIT
// ============================================

// If any future fails, the returned future
// completes with the first error.

Future<int> failing() async {
  await Future.delayed(Duration(milliseconds: 500));
  throw Exception('failed');
}

Future<void> errorHandling() async {
  try {
    await Future.wait([fetchUser(), failing(), fetchOrders()]);
  } catch (e) {
    print('caught: $e'); // caught: Exception: failed
  }
}


// ============================================
// PART 5: EAGERERROR — FAIL FAST
// ============================================

// With eagerError: true, the returned future
// completes as soon as the first error occurs.

Future<void> eagerError() async {
  final stopwatch = Stopwatch()..start();

  try {
    await Future.wait(
      [failing(), fetchUser(), fetchOrders()],
      eagerError: true,
    );
  } catch (e) {
    stopwatch.stop();
    print('Failed after ${stopwatch.elapsedMilliseconds}ms'); // ~500ms
    // Without eagerError, this would be ~1000ms (waiting for all)
  }
}


// ============================================
// PART 6: FUTURE.ANY — THE FASTEST WINS
// ============================================

// Future.any returns the result of the first
// future to complete. The others are discarded.

Future<String> cache() async {
  await Future.delayed(Duration(milliseconds: 300));
  return 'cache';
}

Future<String> server() async {
  await Future.delayed(Duration(seconds: 1));
  return 'server';
}

Future<void> race() async {
  final result = await Future.any([server(), cache()]);
  print(result); // cache
}


// ============================================
// PART 7: FUTURE.ANY WITH ERRORS
// ============================================

// The first future to complete wins, whether
// with a value or an error.

Future<String> failFast() async {
  await Future.delayed(Duration(milliseconds: 100));
  throw Exception('fast failure');
}

Future<void> raceWithError() async {
  try {
    await Future.any([server(), failFast()]);
  } catch (e) {
    print(e); // Exception: fast failure
  }
}


// ============================================
// PART 8: EMPTY ITERABLE BEHAVIOR
// ============================================

// Future.any on an empty iterable never completes.
// Future.wait on an empty iterable completes
// immediately with an empty list.

Future<void> emptyAny() async {
  final result = await Future.any([]);
  print(result); // never reached
}

Future<void> emptyWait() async {
  final result = await Future.wait([]);
  print(result); // []
}


// ============================================
// PART 9: CLEANUP ON ERROR
// ============================================

// The cleanUp function releases resources from
// successful futures when an error occurs.

Future<void> cleanupExample() async {
  await Future.wait(
    [fetchUser(), failing(), fetchOrders()],
    cleanUp: (result) {
      print('cleaning up: $result');
    },
  ).catchError((e) {
    print('error: $e');
  });
}
// Output:
// cleaning up: User
// error: Exception: failed


// ============================================
// PART 10: THE DASHBOARD EXAMPLE
// ============================================

// A dashboard that loads three independent
// data sets in parallel.

Future<Map<String, dynamic>> loadDashboard() async {
  final results = await Future.wait([
    fetchUser(),
    fetchOrders(),
    fetchPromos(),
  ]);

  return {
    'user': results[0],
    'orders': results[1],
    'promos': results[2],
  };
}

The ten parts show sequential versus parallel execution, order preservation, error handling, eagerError, Future.any, races with errors, empty iterable behavior, cleanup, and a complete dashboard example.


Quick Reference

Future.wait vs Future.any

AspectFuture.waitFuture.any
CompletionWhen all futures completeWhen first future completes
ResultList of all valuesValue of the first future
OrderPreserved from inputNot applicable (single value)
On errorCompletes with first errorCompletes with first error
eagerErrorSupportedNot applicable
cleanUpSupportedNot supported
Empty iterableCompletes with []Never completes

Future.wait Parameters

ParameterDefaultEffect
eagerErrorfalseIf true, completes on first error immediately
cleanUpnullCalled on successful results when an error occurs

When to Use Each

ScenarioMethod
Need all results (dashboard, batch)Future.wait
Need first result (cache, fallback)Future.any
Need each result as it arrivesthen on each future
Fail fast on any errorFuture.wait with eagerError: true
Race with fallbackFuture.any

Best Practices

✅ Do This:

// Create all futures before awaiting
final results = await Future.wait([a(), b(), c()]);                    // ✅
// Use Future.wait when you need all results
final [user, posts] = await Future.wait([fetchUser(), fetchPosts()]);  // ✅
// Use Future.any for races
final fastest = await Future.any([cache(), server()]);                 // ✅
// Use eagerError for fail-fast behavior
await Future.wait(tasks, eagerError: true);                            // ✅
// Use cleanUp to release resources on error
await Future.wait(tasks, cleanUp: (r) => r.close());                   // ✅
// Check that the iterable is not empty before Future.any
if (futures.isNotEmpty) await Future.any(futures);                     // ✅

❌ Don’t Do This:

// Don't await futures sequentially when they are independent
final a = await fetchA(); final b = await fetchB(); // slow          // ❌
// Don't use Future.any when you need all results
await Future.any([fetchA(), fetchB()]); // only one result            // ❌
// Don't assume Future.any waits for all
await Future.any([slow(), fast()]); // returns fast, slow discarded   // ❌
// Don't forget to handle errors with Future.wait
await Future.wait([failing(), other()]); // unhandled error          // ❌
// Don't pass an empty iterable to Future.any
await Future.any([]); // hangs forever                               // ❌

Common Pitfalls

PitfallWhy It HappensFix
Operations run sequentiallyFutures created inside await expressionCreate futures before awaiting
Results in wrong orderAssuming completion orderFuture.wait preserves input order
Error not caughtNo try/catch around Future.waitWrap in try/catch
eagerError not workingDefault is falsePass eagerError: true
Future.any never completesEmpty iterableCheck futures.isNotEmpty
Losing futures not cleaned upFuture.any discards resultsAttach then to losing futures or use Future.wait
Resources leaked on errorNo cleanUpProvide cleanUp function

Real-World Examples

1. Parallel Dashboard Load

final results = await Future.wait([fetchUser(), fetchOrders(), fetchPromos()]);

2. Fail Fast

await Future.wait(tasks, eagerError: true);

3. Race with Cache

final data = await Future.any([cache(), server()]);

4. Order Preservation

final results = await Future.wait([slow(), fast()]); // [slow, fast]

5. Cleanup on Error

await Future.wait(tasks, cleanUp: (r) => r.close());

6. Empty Wait

final results = await Future.wait([]); // []

7. Race with Fallback

final value = await Future.any([primary(), fallback()]);

8. Sequential vs Parallel

// Parallel: ~1 second total
final results = await Future.wait([a(), b(), c()]);

9. Error Handling

try { await Future.wait([a(), b()]); } catch (e) { print(e); }

10. Collect Results

final [user, orders] = await Future.wait([fetchUser(), fetchOrders()]);

Visual

Sequential vs Parallel Execution

┌──────────────────────────────────────────────────────────────┐
│  SEQUENTIAL vs PARALLEL                                      │
│                                                              │
│  SEQUENTIAL:                                                 │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  fetchA() ──1s──►                                    │    │
│  │  fetchB() ──────────2s──►                            │    │
│  │  fetchC() ────────────────3s──►                      │    │
│  │                                                       │    │
│  │  Total: 6 seconds                                    │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  PARALLEL:                                                   │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  fetchA() ──1s──►                                    │    │
│  │  fetchB() ──────────2s──►                            │    │
│  │  fetchC() ────────────────3s──►                      │    │
│  │                                                       │    │
│  │  Total: 3 seconds (the longest operation)            │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  All futures start at the same time. The wait is for all    │
│  to complete. The total time is the slowest.                │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Future.wait vs Future.any

┌──────────────────────────────────────────────────────────────┐
│  FUTURE.WAIT vs FUTURE.ANY                                   │
│                                                              │
│  Future.wait([A, B, C]):                                     │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  A ──► completes                                    │    │
│  │  B ──────► completes                                │    │
│  │  C ────────────► completes                          │    │
│  │                                                       │    │
│  │  Wait for ALL. Result: [A, B, C]                    │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  Future.any([A, B, C]):                                      │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  A ──► completes FIRST                              │    │
│  │  B ──────► (discarded)                              │    │
│  │  C ────────────► (discarded)                        │    │
│  │                                                       │    │
│  │  Wait for FIRST. Result: A's value                  │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  wait = completeness. any = speed.                           │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Error Handling Comparison

┌──────────────────────────────────────────────────────────────┐
│  ERROR HANDLING                                              │
│                                                              │
│  Future.wait (eagerError: false):                            │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  A ──► success (1s)                                 │    │
│  │  B ──────► ERROR (2s)                               │    │
│  │  C ────────────► success (3s)                       │    │
│  │                                                       │    │
│  │  Waits for ALL, then reports B's error at 3s         │    │
│  │  cleanUp runs on A's value                           │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  Future.wait (eagerError: true):                             │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  A ──► success (1s)                                 │    │
│  │  B ──────► ERROR (2s) → completes immediately        │    │
│  │  C ────────────► (still running)                    │    │
│  │                                                       │    │
│  │  Reports B's error at 2s, does not wait for C        │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  Future.any:                                                 │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  A ──► success (1s) → completes with A's value       │    │
│  │  B ──────► ERROR (2s) → discarded                    │    │
│  │  C ────────────► success (3s) → discarded            │    │
│  │                                                       │    │
│  │  First completion wins, whether value or error       │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Race with Cache and Server

┌──────────────────────────────────────────────────────────────┐
│  RACE WITH CACHE AND SERVER                                  │
│                                                              │
│  Future.any([cache(), server()])                             │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  cache()  ──300ms──► completes                       │    │
│  │  server() ──────────1s──► (discarded)                │    │
│  │                                                       │    │
│  │  Result: cache's value at 300ms                      │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  cache()  ──300ms──► ERROR                           │    │
│  │  server() ──────────1s──► (discarded)                │    │
│  │                                                       │    │
│  │  Result: cache's error at 300ms                      │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  The first completion wins, whether value or error.          │
│  The server result is never used, even if it succeeds.       │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Future.waitRuns futures in parallel; waits for all to complete
Future.wait resultList of values in input order
Future.wait errorCompletes with first error
eagerErrorIf true, completes on first error immediately
cleanUpCalled on successful results when an error occurs
Future.anyRuns futures in parallel; returns first completion
Future.any resultValue (or error) of the first future to complete
Future.any emptyNever completes
Use Future.waitWhen all results are needed
Use Future.anyWhen only the fastest result matters

Key takeaways:

  • Parallel execution overlaps independent operations. Creating futures before awaiting them allows them to run concurrently. The total time is the longest operation, not the sum .
  • Future.wait preserves input order in the result list. The first future’s value is at index 0, the second at index 1, regardless of which completed first .
  • Future.wait completes with the first error by default, after waiting for all futures. eagerError: true changes this to complete immediately on the first error .
  • cleanUp releases resources from successful futures on error. The error path does not provide access to successful values, so cleanUp is the only way to close them .
  • Future.any returns the first completion, whether value or error. The other futures are discarded. The method does not distinguish between success and failure; it only cares about speed .
  • Future.any on an empty iterable never completes. This is a silent hang. Always check that the iterable is not empty before calling Future.any .
  • Use Future.wait for completeness and Future.any for speed. The choice is determined by whether the operation needs every result or only the first one .
  • Sequential awaits are the default; parallel awaits require explicit futures. A sequence of await expressions runs sequentially because the second future is not created until the first completes. Future.wait creates all futures upfront, enabling concurrency.

Remember: Future.wait and Future.any are the two methods for parallel future execution, and they answer different questions. Future.wait is about completeness: it waits for every future, collects every result in input order, and fails with the first error. Future.any is about speed: it returns the first completion, discards the rest, and completes with either a value or an error depending on which future wins the race. The choice between them is the choice between needing all results and needing the fastest one. Both methods start the futures immediately when the iterable is created, so the parallelism is real. The error semantics follow from the purpose: Future.wait cannot succeed if any future fails, so it reports the error; Future.any only needs one result, so the first completion — value or error — is the result. Understanding these differences makes it possible to write asynchronous code that runs independent operations concurrently, which is the difference between waiting three times as long as necessary and waiting only as long as the slowest operation.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!