Watch the video (10 min)
On this page23 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. Measuring Time with Stopwatch
  5. 2. Sequential Awaiting
  6. 3. Start First, Await Later
  7. 4. Future.wait with a List
  8. 5. Different Types: (a, b).wait
  9. 6. When Sequential Is Right
  10. 7. When One Future Fails
  11. 8. Failing Fast with eagerError
  12. 9. Errors from a Record Wait: ParallelWaitError
  13. 10. The First Finisher: Future.any
  14. 11. A Real-World Example
  15. Code Examples
  16. Common Beginner Mistakes
  17. Best Practices
  18. Mini Challenge
  19. Practice Questions
  20. Practice Answers
  21. Quick Revision
  22. Key Takeaways
  23. What's Next?

Lecture 91 — Working with Multiple Futures

Category15 — Asynchronous Programming
Lecture091 of 140
Estimated video length10 min
TargetDart 3.12.2
LevelIntermediate

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 ms will 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 with var (name, marks) = ...
  • try, catch, and on from the exception lectures
  • Future, Future.delayed, and Future.value (Lecture 088)
  • async and await (Lecture 089)
  • catching errors from a future with try/catch around await (Lecture 090)

Connection With Previous Lectures

So far every program has awaited one future at a time:

dart
var name = await loadName(); // 1 second
var marks = await loadMarks(); // 2 seconds

Loading 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 ParallelWaitError from 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:

dart
void main() async {
  var stopwatch = Stopwatch()..start();
  await Future.delayed(Duration(seconds: 1));
  print('Took ${stopwatch.elapsedMilliseconds} ms');
}
output
Took 1004 ms

The 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:

dart
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');
}
output
Sara scored 88
Took 3010 ms

1 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):

dart
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');
}
output
Sara scored 88
Took 2004 ms

Both 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:

dart
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');
}
output
Loaded 95
Loaded 70
Loaded 80
[80, 95, 70]
Took 3006 ms

The 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:

dart
void main() async {
  var (name, marks) = await (loadName(), loadMarks()).wait;
  print('$name scored ${marks + 2}');
}
output
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:

dart
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');
}
output
Student 7: 91

Ask 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:

dart
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');
  }
}
output
ok after 3 s
Caught: Exception: A

A 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):

dart
void main() async {
  try {
    await Future.wait([ok(3), fail('A', 1)], eagerError: true);
  } catch (e) {
    print('Caught: $e');
  }
}
output
Caught: Exception: A
ok after 3 s

The 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):

dart
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);
  }
}
output
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:

dart
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);
}
output
Price from Server 2

Like 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:

dart
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');
}
output
Sara: balance 2500.0, spent 450.0
Took 2005 ms

Three tasks, three types, one wait — about 2 seconds instead of 4.


Code Examples

Example 1 — Sequential Total

dart
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');
}
output
Took 2009 ms

Example 2 — Parallel Total

dart
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');
}
output
Took 1004 ms

Example 3 — Future.wait Keeps Your Order

dart
void main() async {
  var words = await Future.wait([
    Future.delayed(Duration(seconds: 2), () => 'Ali'),
    Future.delayed(Duration(seconds: 1), () => 'Sara'),
  ]);
  print(words);
}
output
[Ali, Sara]

Example 4 — Building the List with for

dart
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);
}
output
[1, 4, 9]

Example 5 — A Record Wait

dart
void main() async {
  var (city, temp) = await (Future.value('Lahore'), Future.value(31)).wait;
  print('$city: $temp°C');
}
output
Lahore: 31°C

Example 6 — Future.any

dart
void main() async {
  var winner = await Future.any([
    Future.delayed(Duration(seconds: 2), () => 'Ali'),
    Future.delayed(Duration(seconds: 1), () => 'Zayan'),
  ]);
  print('$winner answered first');
}
output
Zayan answered first

Example 7 — Student Marks

dart
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');
}
output
Marks: [90, 85, 78], total: 253

Example 8 — Shopping Total

dart
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}');
}
output
Total: 1650.0

Example 9 — Bank Balance with a Failing Source

dart
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}');
  }
}
output
Partial: (2000.0, null)
Errors: (null, Exception: Bank offline)

Example 10 — Fastest Temperature Sensor

dart
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');
}
output
Temperature: 24.8°C

Common Beginner Mistakes

1. Awaiting independent tasks one by one

dart
var name = await loadName(); // 1 s
var marks = await loadMarks(); // 2 s — total about 3 s

No 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

dart
void main() {
  var marks = Future.wait([Future.value(80), Future.value(95)]);
  print(marks);
}
output
Instance of 'Future<List<int>>'

Without await you print the future itself, not the list inside it.

3. Mixing types in Future.wait

dart
void main() async {
  var results = await Future.wait([Future.value('Sara'), Future.value(88)]);
  // int marks = results[1]; // ✗
}
output
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

dart
void main() async {
  try {
    await (Future.value(1), Future<int>.error(Exception('x'))).wait;
  } on Exception catch (e) {
    print('never reached: $e');
  }
}
output
Unhandled exception:
ParallelWaitError: Exception: x

ParallelWaitError 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.wait for many futures of the same type, and a record .wait for a few of different types.
  • Wrap combined waits in try/catch — one failure fails the whole wait.
  • Use eagerError: true when there is no point waiting once something has failed.
  • From a record wait, read e.values to 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
dart
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');
}
output
Temperatures: [33.0, 31.0, 18.0]
Average: 27.333333333333332
Took 3007 ms

How 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

  1. Use Stopwatch to time a 2-second Future.delayed and print the milliseconds.
  2. Use Future.wait to wait for Future.value(10), Future.value(20), and Future.value(30) and print the list.
  3. Use a record .wait to wait for Future.value('Ali') and Future.value(true) and print Ali present: true.

Application

  1. Two functions each take 1 second. Write the sequential version and the parallel version, and print the time of each.

  2. What does this print, and in what order?

    dart
    void main() async {
      var r = await Future.wait([
        Future.delayed(Duration(seconds: 2), () => 'slow'),
        Future.delayed(Duration(seconds: 1), () => 'fast'),
      ]);
      print(r);
    }
  3. Load the prices 250.0, 100.0, and 75.0 with Future.wait from a function that waits 300 ms each, and print Total: 425.0.

  4. Use Future.any to print whichever of 'Ali' (2 s) or 'Sara' (1 s) finishes first.

Challenge

  1. In Future.wait([fail('X', 2), fail('Y', 1)]), which error is caught by default? Write the program to check.
  2. Write a record wait of Future.value(500.0) and a future that throws Exception('Card blocked'). Catch the ParallelWaitError and print only the first value.
  3. findStudentId takes 1 s, then loadMarksFor(id) takes 1 s, and separately loadSchoolName() takes 2 s. Write a program that finishes in about 2 seconds, not 3.

Practice Answers

Question 1 — Solution

dart
void main() async {
  var stopwatch = Stopwatch()..start();
  await Future.delayed(Duration(seconds: 2));
  print('Took ${stopwatch.elapsedMilliseconds} ms');
}
output
Took 2004 ms

The exact number varies a little each run.

Question 2 — Solution

dart
void main() async {
  var numbers = await Future.wait([
    Future.value(10),
    Future.value(20),
    Future.value(30),
  ]);
  print(numbers);
}
output
[10, 20, 30]

Question 3 — Solution

dart
void main() async {
  var (name, present) = await (Future.value('Ali'), Future.value(true)).wait;
  print('$name present: $present');
}
output
Ali present: true

Question 4 — Solution

dart
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');
}
output
Sequential: 2007 ms
Parallel: 1002 ms

reset() sets the stopwatch back to zero while it keeps running.

Question 5 — Solution

output
[slow, fast]

'fast' finishes first, but the list follows the written order. It prints once, after about 2 seconds.

Question 6 — Solution

dart
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)}');
}
output
Total: 425.0

Question 7 — Solution

dart
void main() async {
  var first = await Future.any([
    Future.delayed(Duration(seconds: 2), () => 'Ali'),
    Future.delayed(Duration(seconds: 1), () => 'Sara'),
  ]);
  print(first);
}
output
Sara

Question 8 — Solution

dart
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');
  }
}
output
Caught: Exception: Y

Y fails first in time, so its error wins — list position does not matter.

Question 9 — Solution

dart
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');
  }
}
output
Balance: 500.0

A 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

dart
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');
}
output
City School: 91
Took 2006 ms

The two dependent steps stay sequential inside loadMarks, while that chain runs in parallel with loadSchoolName.


Quick Revision

  1. What does calling an async function do before you await it?
  2. How long does awaiting a 1 s and a 2 s task take sequentially? In parallel?
  3. In what order does Future.wait return its results?
  4. Why use (a, b).wait instead of Future.wait for a String and an int?
  5. Which error does Future.wait throw by default when two futures fail?
  6. What does eagerError: true change?
  7. What do values and errors hold in a ParallelWaitError?
  8. Does Future.any stop the slower futures?

Key Takeaways

  • Calling an async function starts the work; await only 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).wait waits for 2–9 futures of different types and keeps each type.
  • Future.wait fails with the first error to happen; eagerError: true reports it at once.
  • A record .wait throws a ParallelWaitError with every value and every error.
  • Future.any gives 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.


Video lecture

Subscribe
Placeholder video — the real recording isn't linked yet.