ZetCode

Dart Future Combinators

last modified October 18, 2026

In this article we explore Future combinators in Dart, the static helpers that coordinate many futures at once: waiting for all of them, taking the first, or running them one by one.

Future Combinators Overview

A Future represents a single asynchronous result. Real programs usually juggle several at a time, and combining them by hand with nested await calls quickly becomes awkward. Dart's combinators express the common coordination patterns directly: run a group in parallel and collect the results, race them and keep the first, or process a collection sequentially.

Combinator Purpose Completes with
Future.wait Run futures in parallel List<T> of all values
Future.any Take the first to complete The first value
Future.forEach Process a collection sequentially Completion when all are done
Future.doWhile Repeat while a condition holds Completion when the loop ends

Future.wait: Awaiting Many

Future.wait takes an iterable of futures and returns a single future that completes with a list of their results in the same order. It does not start the operations itself. When the operations are started together before awaiting, they can overlap, so the total time is roughly that of the slowest one rather than their sum.

future_wait.dart
Future<int> fetch(int id) async {
  await Future.delayed(const Duration(milliseconds: 50));
  return id * 2;
}

Future<void> main() async {
  final results = await Future.wait([fetch(1), fetch(2), fetch(3)]);
  print(results);
}

The three fetch calls start their operations while the argument list is built, before Future.wait is called. The combinator waits for their results. The returned list preserves the input order, so the results are [2, 4, 6] regardless of which future finishes first.

$ dart run future_wait.dart
[2, 4, 6]

Handling Errors in Future.wait

If any future passed to Future.wait fails, the combined future fails too. By default, eagerError is false: it waits for all input futures to finish, then reports the first error. Setting eagerError: true reports the first error as soon as it occurs. Neither mode cancels the remaining operations.

future_wait_error.dart
Future<int> fetch(int id) async {
  await Future.delayed(Duration(milliseconds: id * 20));
  if (id == 2) throw StateError('failed $id');
  return id;
}

Future<void> main() async {
  try {
    await Future.wait(
      [fetch(1), fetch(2), fetch(3)],
      eagerError: true,
    );
  } catch (e) {
    print('caught: $e');
  }

  final settled = await Future.wait(
    [fetch(1), fetch(2), fetch(3)],
    eagerError: false,
  ).catchError((e) => <int>[]);
  print('eager false: $settled');
}

The first block uses eagerError: true, so it catches the StateError without waiting for fetch(3) to finish. That operation still continues. The second explicitly uses the default, eagerError: false, so all its input futures finish before the error is reported. It then converts the failure into an empty list with catchError; successful results are not returned as partial results.

$ dart run future_wait_error.dart
caught: Bad state: failed 2
eager false: []

Future.any: The First Result

Future.any races an iterable of futures and completes with the value or error of the first one to finish. It selects the first completion, not the first successful result: a fast failure wins even if another source later succeeds. The remaining operations are not cancelled.

future_any.dart
Future<String> delayed(String value, int ms) async {
  await Future.delayed(Duration(milliseconds: ms));
  return value;
}

Future<void> main() async {
  final fastest = await Future.any([
    delayed('slow', 200),
    delayed('fast', 50),
    delayed('medium', 100),
  ]);
  print(fastest);
}

Although 'slow' is listed first, Future.any does not care about order: it resolves with whichever future completes first. With delays of 200, 50, and 100 milliseconds, the 50 millisecond future wins and fast is printed.

$ dart run future_any.dart
fast

Future.forEach: Sequential Iteration

Future.forEach applies an asynchronous action to each element of a collection, one after another, and completes when the last one is done. Unlike Future.wait, the operations do not overlap.

future_foreach.dart
Future<void> process(int n) async {
  await Future.delayed(const Duration(milliseconds: 20));
  print('processed $n');
}

Future<void> main() async {
  await Future.forEach([1, 2, 3], (n) => process(n));
  print('all done');
}

The three items are processed in order, each waiting for the previous to finish before it starts. The output appears in sequence, and all done prints only after the whole iteration completes. This ordering makes Future.forEach the right tool when tasks must not overlap.

$ dart run future_foreach.dart
processed 1
processed 2
processed 3
all done

Future.doWhile: Repeated Async Work

Future.doWhile repeatedly runs an asynchronous callback for as long as it returns true. It is the asynchronous counterpart of a do-while loop and is handy for polling or retrying.

future_dowhile.dart
Future<void> main() async {
  var attempts = 0;
  await Future.doWhile(() async {
    attempts++;
    await Future.delayed(const Duration(milliseconds: 20));
    print('attempt $attempts');
    return attempts < 3;
  });
  print('stopped after $attempts');
}

The callback increments the counter, waits, prints, and then returns whether to continue. After the third attempt it returns false, the loop stops, and the final message reports the number of attempts. Because the condition is evaluated after the body, the block always runs at least once.

$ dart run future_dowhile.dart
attempt 1
attempt 2
attempt 3
stopped after 3

Future Constructors and then/catchError

Besides coordinating existing futures, the Future class provides constructors for creating them directly: Future.value for an immediate value, Future.delayed for a delayed value, and Future.error for an immediate failure. The older then/catchError style still works alongside await.

future_constructors.dart
Future<void> main() async {
  final v = await Future.value(10);
  print(v);

  final d = await Future.delayed(const Duration(milliseconds: 20), () => 'ok');
  print(d);

  await Future.error(StateError('nope')).catchError((e) {
    print('handled: ${e.message}');
    return null;
  });

  final chain = await Future.value(2).then((x) => x * 5);
  print(chain);
}

Future.value(10) completes with 10 immediately, and Future.delayed produces 'ok' after a short pause. Future.error fails right away, and catchError recovers from it, printing the message. The final line chains a transformation with then, multiplying by five to yield 10.

$ dart run future_constructors.dart
10
ok
handled: nope
10

Source

Dart API: Future
Dart API: Future.wait
Dart API: Future.any
Dart API: Future.forEach

Future combinators capture the usual ways to coordinate asynchronous work. Future.wait runs many futures in parallel and gathers their results, Future.any races them, while Future.forEach and Future.doWhile run work sequentially. Together with the Future constructors and the then/catchError style, they cover most coordination needs beyond a single await.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Dart tutorials.