On this page23 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. Measuring Time with Stopwatch
- 2. Sequential Awaiting
- 3. Start First, Await Later
- 4. Future.wait with a List
- 5. Different Types: (a, b).wait
- 6. When Sequential Is Right
- 7. When One Future Fails
- 8. Failing Fast with eagerError
- 9. Errors from a Record Wait: ParallelWaitError
- 10. The First Finisher: Future.any
- 11. A Real-World Example
- Code Examples
- Common Beginner Mistakes
- Best Practices
- Mini Challenge
- Practice Questions
- Practice Answers
- Quick Revision
- Key Takeaways
- What's Next?
Lecture 91 — Working with Multiple Futures
| Category | 15 — Asynchronous Programming |
| Lecture | 091 of 140 |
| Estimated video length | 10 min |
| Target | Dart 3.12.2 |
| Level | Intermediate |
Awaiting three slow tasks one after another makes you wait for all three in a row. Starting them together and waiting once makes the total only as long as the slowest.
Contents
Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways
Every output and error message in this lecture is real. Each one was produced by running the code shown on Dart 3.12.2. Timing lines are approximate: a line such as
Took 2005 mswill show a slightly different number on every run and every computer, so read it as "about 2 seconds".
Prerequisites
Students should already understand, from Lectures 076–090:
- records like
('Sara', 88)and destructuring them withvar (name, marks) = ... try,catch, andonfrom the exception lecturesFuture,Future.delayed, andFuture.value(Lecture 088)asyncandawait(Lecture 089)- catching errors from a future with
try/catcharoundawait(Lecture 090)
Connection With Previous Lectures
So far every program has awaited one future at a time:
var name = await loadName(); // 1 second
var marks = await loadMarks(); // 2 secondsLoading the marks does not need the name, yet the second line cannot start until the first has finished. The program waits 3 seconds for work that could take 2. Real apps load a profile, a balance, and a list of messages at the same moment — they need a way to run independent futures together and collect all the results.
Learning Objectives
By the end of this lecture you will be able to:
- Measure how long code takes with
Stopwatch - Explain the difference between sequential and parallel awaiting
- Start several futures before awaiting any of them
- Wait for a list of futures with
Future.wait - Wait for futures of different types with the record form
(a, b).wait - Predict what happens when one of the futures fails, with and without
eagerError - Read a
ParallelWaitErrorfrom a record wait - Take the first result to arrive with
Future.any
1. Measuring Time with Stopwatch
To compare approaches we need a clock. Stopwatch measures time that passes while your
program runs:
void main() async {
var stopwatch = Stopwatch()..start();
await Future.delayed(Duration(seconds: 1));
print('Took ${stopwatch.elapsedMilliseconds} ms');
}Took 1004 msThe number is a little above 1000 because starting and finishing the delay takes a few milliseconds. Your number will differ slightly — that is normal.
2. Sequential Awaiting
Analogy — making breakfast. You can wait for the kettle to boil and then put bread in the toaster, or switch both on and wait once. The second way finishes as soon as the slower machine does.
Here is the first way in code:
Future<String> loadName() async {
await Future.delayed(Duration(seconds: 1));
return 'Sara';
}
Future<int> loadMarks() async {
await Future.delayed(Duration(seconds: 2));
return 88;
}
void main() async {
var stopwatch = Stopwatch()..start();
var name = await loadName();
var marks = await loadMarks();
print('$name scored $marks');
print('Took ${stopwatch.elapsedMilliseconds} ms');
}Sara scored 88
Took 3010 ms1 second + 2 seconds = about 3 seconds. This is sequential awaiting.
3. Start First, Await Later
Calling an async function starts its work immediately and hands back a future. await
only waits for a result. So start both, then wait (same loadName and loadMarks):
void main() async {
var stopwatch = Stopwatch()..start();
var nameFuture = loadName(); // starts now
var marksFuture = loadMarks(); // also starts now
var name = await nameFuture;
var marks = await marksFuture;
print('$name scored $marks');
print('Took ${stopwatch.elapsedMilliseconds} ms');
}Sara scored 88
Took 2004 msBoth delays run at the same time, so the total is the slowest one — about 2 seconds. This is parallel awaiting.
4. Future.wait with a List
Future.wait takes a list of futures and returns one future that completes with a list
of all their results:
Future<int> loadMark(int mark, int seconds) async {
await Future.delayed(Duration(seconds: seconds));
print('Loaded $mark');
return mark;
}
void main() async {
var stopwatch = Stopwatch()..start();
var marks = await Future.wait([
loadMark(80, 3),
loadMark(95, 1),
loadMark(70, 2),
]);
print(marks);
print('Took ${stopwatch.elapsedMilliseconds} ms');
}Loaded 95
Loaded 70
Loaded 80
[80, 95, 70]
Took 3006 msThe futures finish in the order 95, 70, 80, but the result list follows the order you wrote them in. The total is about 3 seconds — the slowest — not 6.
5. Different Types: (a, b).wait
Future.wait needs one element type for the list. With a String and an int, the results
come back as a List<Object>, and you lose the types. Dart 3 adds .wait on a record of
futures instead:
void main() async {
var (name, marks) = await (loadName(), loadMarks()).wait;
print('$name scored ${marks + 2}');
}Sara scored 90(loadName(), loadMarks()) is a record of a Future<String> and a Future<int>. Its .wait
gives a future of the record (String, int), so name is a String and marks an int —
marks + 2 just works. It works for records of 2 to 9 futures.
6. When Sequential Is Right
Parallel only makes sense when the tasks are independent. If one task needs another's result, it has to wait for it:
Future<int> findStudentId(String name) async {
await Future.delayed(Duration(seconds: 1));
return 7;
}
Future<int> loadMarksFor(int id) async {
await Future.delayed(Duration(seconds: 1));
return 91;
}
void main() async {
var id = await findStudentId('Ali');
var marks = await loadMarksFor(id); // needs id first
print('Student $id: $marks');
}Student 7: 91Ask one question before going parallel: does this task need the result of that one?
7. When One Future Fails
If any future in Future.wait fails, the combined future fails too. By default Future.wait
still waits for every future to finish, then throws the first error that happened:
Future<int> ok(int seconds) async {
await Future.delayed(Duration(seconds: seconds));
print('ok after $seconds s');
return seconds;
}
Future<int> fail(String message, int seconds) async {
await Future.delayed(Duration(seconds: seconds));
throw Exception(message);
}
void main() async {
try {
await Future.wait([ok(3), fail('B', 2), fail('A', 1)]);
} catch (e) {
print('Caught: $e');
}
}ok after 3 s
Caught: Exception: AA failed first (after 1 second), so its error is the one you catch — even though it is
written last. The other results are thrown away.
8. Failing Fast with eagerError
Pass eagerError: true to report the first error as soon as it happens (same ok and
fail):
void main() async {
try {
await Future.wait([ok(3), fail('A', 1)], eagerError: true);
} catch (e) {
print('Caught: $e');
}
}Caught: Exception: A
ok after 3 sThe error arrives after 1 second instead of 3. Notice ok after 3 s still prints: Dart
stops waiting for the other futures, but it does not stop them running.
9. Errors from a Record Wait: ParallelWaitError
A record .wait does not throw the plain exception. It throws a ParallelWaitError that
keeps every result and every error (same ok and fail):
void main() async {
try {
await (ok(1), fail('Network down', 2), Future.value('Sara')).wait;
} on ParallelWaitError catch (e) {
print(e);
print(e.values);
print(e.errors);
}
}ok after 1 s
ParallelWaitError: Exception: Network down
(1, null, Sara)
(null, Exception: Network down, null)values holds the result of each future that succeeded (null where it failed), and
errors holds the error of each one that failed (null where it succeeded). You can still
use the parts that worked.
10. The First Finisher: Future.any
Future.any completes with the result of whichever future finishes first — useful when
two sources can answer the same question:
Future<String> fromServer(String name, int seconds) async {
await Future.delayed(Duration(seconds: seconds));
return 'Price from $name';
}
void main() async {
var price = await Future.any([
fromServer('Server 1', 2),
fromServer('Server 2', 1),
]);
print(price);
}Price from Server 2Like eagerError, the slower future is not stopped — its result is simply ignored.
11. A Real-World Example
Sara's banking dashboard needs her name, balance, and recent payments. None depends on another, so all three load in parallel:
Future<String> loadOwner() async {
await Future.delayed(Duration(seconds: 1));
return 'Sara';
}
Future<double> loadBalance() async {
await Future.delayed(Duration(seconds: 2));
return 2500.0;
}
Future<List<double>> loadPayments() async {
await Future.delayed(Duration(seconds: 1));
return [300.0, 150.0];
}
void main() async {
var stopwatch = Stopwatch()..start();
try {
var (owner, balance, payments) = await (
loadOwner(),
loadBalance(),
loadPayments(),
).wait;
var spent = payments.fold(0.0, (sum, p) => sum + p);
print('$owner: balance $balance, spent $spent');
} on ParallelWaitError catch (e) {
print('Could not load dashboard: ${e.errors}');
}
print('Took ${stopwatch.elapsedMilliseconds} ms');
}Sara: balance 2500.0, spent 450.0
Took 2005 msThree tasks, three types, one wait — about 2 seconds instead of 4.
Code Examples
Example 1 — Sequential Total
Future<int> task(int seconds) =>
Future.delayed(Duration(seconds: seconds), () => seconds);
void main() async {
var stopwatch = Stopwatch()..start();
await task(1);
await task(1);
print('Took ${stopwatch.elapsedMilliseconds} ms');
}Took 2009 msExample 2 — Parallel Total
Future<int> task(int seconds) =>
Future.delayed(Duration(seconds: seconds), () => seconds);
void main() async {
var stopwatch = Stopwatch()..start();
var first = task(1);
var second = task(1);
await first;
await second;
print('Took ${stopwatch.elapsedMilliseconds} ms');
}Took 1004 msExample 3 — Future.wait Keeps Your Order
void main() async {
var words = await Future.wait([
Future.delayed(Duration(seconds: 2), () => 'Ali'),
Future.delayed(Duration(seconds: 1), () => 'Sara'),
]);
print(words);
}[Ali, Sara]Example 4 — Building the List with for
Future<int> square(int n) async {
await Future.delayed(Duration(milliseconds: 100));
return n * n;
}
void main() async {
var squares = await Future.wait([
for (var n in [1, 2, 3]) square(n),
]);
print(squares);
}[1, 4, 9]Example 5 — A Record Wait
void main() async {
var (city, temp) = await (Future.value('Lahore'), Future.value(31)).wait;
print('$city: $temp°C');
}Lahore: 31°CExample 6 — Future.any
void main() async {
var winner = await Future.any([
Future.delayed(Duration(seconds: 2), () => 'Ali'),
Future.delayed(Duration(seconds: 1), () => 'Zayan'),
]);
print('$winner answered first');
}Zayan answered firstExample 7 — Student Marks
Future<int> loadMark(String subject) async {
await Future.delayed(Duration(milliseconds: 500));
return {'Math': 90, 'Science': 85, 'English': 78}[subject]!;
}
void main() async {
var marks = await Future.wait([
loadMark('Math'),
loadMark('Science'),
loadMark('English'),
]);
var total = marks.fold(0, (sum, m) => sum + m);
print('Marks: $marks, total: $total');
}Marks: [90, 85, 78], total: 253Example 8 — Shopping Total
Future<double> priceOf(String item) async {
await Future.delayed(Duration(milliseconds: 300));
return {'Bag': 1500.0, 'Pen': 50.0}[item]!;
}
void main() async {
var (bag, pen) = await (priceOf('Bag'), priceOf('Pen')).wait;
print('Total: ${bag + pen * 3}');
}Total: 1650.0Example 9 — Bank Balance with a Failing Source
Future<double> savings() async => 2000.0;
Future<double> current() async => throw Exception('Bank offline');
void main() async {
try {
var (a, b) = await (savings(), current()).wait;
print('Total: ${a + b}');
} on ParallelWaitError catch (e) {
print('Partial: ${e.values}');
print('Errors: ${e.errors}');
}
}Partial: (2000.0, null)
Errors: (null, Exception: Bank offline)Example 10 — Fastest Temperature Sensor
Future<double> sensor(double reading, int ms) =>
Future.delayed(Duration(milliseconds: ms), () => reading);
void main() async {
var temp = await Future.any([sensor(24.5, 400), sensor(24.8, 150)]);
print('Temperature: $temp°C');
}Temperature: 24.8°CCommon Beginner Mistakes
1. Awaiting independent tasks one by one
var name = await loadName(); // 1 s
var marks = await loadMarks(); // 2 s — total about 3 sNo error appears — the program is just slower than it needs to be. Use Future.wait or a
record .wait when the tasks do not depend on each other.
2. Forgetting await before Future.wait
void main() {
var marks = Future.wait([Future.value(80), Future.value(95)]);
print(marks);
}Instance of 'Future<List<int>>'Without await you print the future itself, not the list inside it.
3. Mixing types in Future.wait
void main() async {
var results = await Future.wait([Future.value('Sara'), Future.value(88)]);
// int marks = results[1]; // ✗
}Error: A value of type 'Object' can't be assigned to a variable of type 'int'.The list became a List<Object>. Use (nameFuture, marksFuture).wait to keep each type.
4. Catching a record wait with on Exception
void main() async {
try {
await (Future.value(1), Future<int>.error(Exception('x'))).wait;
} on Exception catch (e) {
print('never reached: $e');
}
}Unhandled exception:
ParallelWaitError: Exception: xParallelWaitError is an Error, not an Exception, so on Exception lets it through.
Catch it with on ParallelWaitError.
5. Expecting eagerError or Future.any to cancel the others
Both stop waiting, not running. The slower futures still finish and still print, as Steps 8 and 10 showed.
Best Practices
- Start independent futures together; await one after another only when a task needs the previous result.
- Use
Future.waitfor many futures of the same type, and a record.waitfor a few of different types. - Wrap combined waits in
try/catch— one failure fails the whole wait. - Use
eagerError: truewhen there is no point waiting once something has failed. - From a record wait, read
e.valuesto keep the results that did arrive. - Treat timing numbers as approximate; compare "about 2 s" with "about 3 s", not exact milliseconds.
Mini Challenge
Zayan's weather app asks three cities for their temperature. Each takes a different time:
Karachi 33.0 (2 s), Lahore 31.0 (1 s), Quetta 18.0 (3 s). Load all three in parallel,
print the list and the average, and confirm the total time is about 3 seconds.
Try it yourself before opening the solution.
Show solution
Future<double> temperature(double value, int seconds) =>
Future.delayed(Duration(seconds: seconds), () => value);
void main() async {
var stopwatch = Stopwatch()..start();
var temps = await Future.wait([
temperature(33.0, 2),
temperature(31.0, 1),
temperature(18.0, 3),
]);
var average = temps.fold(0.0, (sum, t) => sum + t) / temps.length;
print('Temperatures: $temps');
print('Average: $average');
print('Took ${stopwatch.elapsedMilliseconds} ms');
}Temperatures: [33.0, 31.0, 18.0]
Average: 27.333333333333332
Took 3007 msHow it works — all three start at once, so the wait lasts as long as the slowest city (Quetta, 3 s). The list keeps the written order, not the finishing order.
Practice Questions
Solve each one before looking at the answers.
Beginner
- Use
Stopwatchto time a 2-secondFuture.delayedand print the milliseconds. - Use
Future.waitto wait forFuture.value(10),Future.value(20), andFuture.value(30)and print the list. - Use a record
.waitto wait forFuture.value('Ali')andFuture.value(true)and printAli present: true.
Application
-
Two functions each take 1 second. Write the sequential version and the parallel version, and print the time of each.
-
What does this print, and in what order?
void main() async { var r = await Future.wait([ Future.delayed(Duration(seconds: 2), () => 'slow'), Future.delayed(Duration(seconds: 1), () => 'fast'), ]); print(r); } -
Load the prices
250.0,100.0, and75.0withFuture.waitfrom a function that waits 300 ms each, and printTotal: 425.0. -
Use
Future.anyto print whichever of'Ali'(2 s) or'Sara'(1 s) finishes first.
Challenge
- In
Future.wait([fail('X', 2), fail('Y', 1)]), which error is caught by default? Write the program to check. - Write a record wait of
Future.value(500.0)and a future that throwsException('Card blocked'). Catch theParallelWaitErrorand print only the first value. findStudentIdtakes 1 s, thenloadMarksFor(id)takes 1 s, and separatelyloadSchoolName()takes 2 s. Write a program that finishes in about 2 seconds, not 3.
Practice Answers
Question 1 — Solution
void main() async {
var stopwatch = Stopwatch()..start();
await Future.delayed(Duration(seconds: 2));
print('Took ${stopwatch.elapsedMilliseconds} ms');
}Took 2004 msThe exact number varies a little each run.
Question 2 — Solution
void main() async {
var numbers = await Future.wait([
Future.value(10),
Future.value(20),
Future.value(30),
]);
print(numbers);
}[10, 20, 30]Question 3 — Solution
void main() async {
var (name, present) = await (Future.value('Ali'), Future.value(true)).wait;
print('$name present: $present');
}Ali present: trueQuestion 4 — Solution
Future<void> task() => Future.delayed(Duration(seconds: 1));
void main() async {
var stopwatch = Stopwatch()..start();
await task();
await task();
print('Sequential: ${stopwatch.elapsedMilliseconds} ms');
stopwatch.reset();
await Future.wait([task(), task()]);
print('Parallel: ${stopwatch.elapsedMilliseconds} ms');
}Sequential: 2007 ms
Parallel: 1002 msreset() sets the stopwatch back to zero while it keeps running.
Question 5 — Solution
[slow, fast]'fast' finishes first, but the list follows the written order. It prints once, after about
2 seconds.
Question 6 — Solution
Future<double> price(double value) =>
Future.delayed(Duration(milliseconds: 300), () => value);
void main() async {
var prices = await Future.wait([price(250.0), price(100.0), price(75.0)]);
print('Total: ${prices.fold(0.0, (sum, p) => sum + p)}');
}Total: 425.0Question 7 — Solution
void main() async {
var first = await Future.any([
Future.delayed(Duration(seconds: 2), () => 'Ali'),
Future.delayed(Duration(seconds: 1), () => 'Sara'),
]);
print(first);
}SaraQuestion 8 — Solution
Future<int> fail(String message, int seconds) async {
await Future.delayed(Duration(seconds: seconds));
throw Exception(message);
}
void main() async {
try {
await Future.wait([fail('X', 2), fail('Y', 1)]);
} catch (e) {
print('Caught: $e');
}
}Caught: Exception: YY fails first in time, so its error wins — list position does not matter.
Question 9 — Solution
Future<double> blocked() async => throw Exception('Card blocked');
void main() async {
try {
await (Future.value(500.0), blocked()).wait;
} on ParallelWaitError catch (e) {
var (balance, _) = e.values as (double?, double?);
print('Balance: $balance');
}
}Balance: 500.0A plain on ParallelWaitError does not tell Dart the record's type, so as states it before
destructuring. Only the first value is needed; _ skips the second.
Question 10 — Solution
Future<int> findStudentId() => Future.delayed(Duration(seconds: 1), () => 7);
Future<int> loadMarksFor(int id) =>
Future.delayed(Duration(seconds: 1), () => 91);
Future<String> loadSchoolName() =>
Future.delayed(Duration(seconds: 2), () => 'City School');
Future<int> loadMarks() async {
var id = await findStudentId();
return loadMarksFor(id);
}
void main() async {
var stopwatch = Stopwatch()..start();
var (marks, school) = await (loadMarks(), loadSchoolName()).wait;
print('$school: $marks');
print('Took ${stopwatch.elapsedMilliseconds} ms');
}City School: 91
Took 2006 msThe two dependent steps stay sequential inside loadMarks, while that chain runs in
parallel with loadSchoolName.
Quick Revision
- What does calling an
asyncfunction do before youawaitit? - How long does awaiting a 1 s and a 2 s task take sequentially? In parallel?
- In what order does
Future.waitreturn its results? - Why use
(a, b).waitinstead ofFuture.waitfor aStringand anint? - Which error does
Future.waitthrow by default when two futures fail? - What does
eagerError: truechange? - What do
valuesanderrorshold in aParallelWaitError? - Does
Future.anystop the slower futures?
Key Takeaways
- Calling an
asyncfunction starts the work;awaitonly waits for it. - Sequential awaiting adds the times; parallel awaiting takes as long as the slowest task.
Future.wait([...])returns all results in the written order.(a, b).waitwaits for 2–9 futures of different types and keeps each type.Future.waitfails with the first error to happen;eagerError: truereports it at once.- A record
.waitthrows aParallelWaitErrorwith every value and every error. Future.anygives the first result; the other futures keep running.- Stay sequential only when one task needs another's result.
What's Next?
Lecture 092 — Streams. A future delivers one value, once. But a chat app receives
message after message, and a sensor sends a new temperature every second. Next we will meet
streams — Dart's way to deliver many values over time — and listen to them with await for.