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<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<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<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<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<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<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
List all Dart tutorials.