Watch the video (10 min)
On this page22 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. A Function That Produces Many Values
  5. 2. sync* and yield
  6. 3. Generators Are Lazy
  7. 4. An Endless Generator
  8. 5. async* — Values Over Time
  9. 6. The Body Runs on Listen
  10. 7. yield* — Pass On Another Generator
  11. 8. Cancellation Stops the Generator
  12. 9. Errors from a Generator
  13. 10. A Real-World Example
  14. Code Examples
  15. Common Beginner Mistakes
  16. Best Practices
  17. Mini Challenge
  18. Practice Questions
  19. Practice Answers
  20. Quick Revision
  21. Key Takeaways
  22. What's Next?

Lecture 96 — Async Generators

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

A normal function hands back one value and stops. A generator hands back values one at a time with yield — and an async* generator turns that into a stream, written like an ordinary loop.

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.


Prerequisites

Students should already understand, from Lectures 043–095:

  • Iterable, its laziness, take, and toList() (Lectures 043 and 046)
  • try/catch/finally (Lecture 084)
  • Future.delayed, async, and await (Lectures 088–089)
  • Stream, await for, listen, and cancelling a subscription (Lectures 092–095)

Connection With Previous Lectures

To make a stream so far, we needed a StreamController: create it, add values, remember to close it, and hand out its stream. That is a lot of machinery for "send 3, then 2, then 1, one second apart". What we really want is to write the countdown as a plain loop and have each value flow out of the function as the loop reaches it.


Learning Objectives

By the end of this lecture you will be able to:

  • Write a sync* generator that returns an Iterable with yield
  • Explain why a generator's body runs only when someone asks for values
  • Write an async* generator that returns a Stream
  • Use await inside an async* generator to space values out over time
  • Pass on every value of another generator with yield*
  • Show that cancelling a subscription stops the generator
  • Build countdowns, tickers, and paginated loading with generators

1. A Function That Produces Many Values

A generator is a function that produces a sequence of values, one by one, instead of a single result.

Analogy — a normal function is a bakery that bakes the whole day's bread at 6 a.m. and hands it all over. A generator bakes one loaf each time a customer asks, and stops baking when the customers stop coming.

Dart has two kinds: sync* for values available right away, and async* for values that arrive over time.


2. sync* and yield

Mark the function sync*, give it the return type Iterable<T>, and send out each value with yield:

dart
Iterable<int> countTo(int n) sync* {
  for (var i = 1; i <= n; i++) {
    yield i;
  }
}

void main() {
  print(countTo(3));
  for (var number in countTo(3)) {
    print('Got $number');
  }
}
output
(1, 2, 3)
Got 1
Got 2
Got 3

yield hands out one value and the function continues — unlike return, which ends it. The round brackets show the result is an Iterable, just like map and where.


3. Generators Are Lazy

Calling a generator does not run its body. The body runs only while someone walks through the values, and pauses at each yield:

dart
Iterable<String> students() sync* {
  print('Generator started');
  yield 'Ali';
  print('After Ali');
  yield 'Sara';
  print('After Sara');
  yield 'Zayan';
}

void main() {
  var names = students();
  print('Nothing has run yet');
  for (var name in names) {
    print('Hello, $name');
  }
}
output
Nothing has run yet
Generator started
Hello, Ali
After Ali
Hello, Sara
After Sara
Hello, Zayan

The lines interleave: the generator yields 'Ali', the loop prints it, and only then does the generator continue to 'After Ali'.


4. An Endless Generator

Because values are produced only on request, a generator may loop forever — the caller decides how many it takes:

dart
Iterable<int> evenNumbers() sync* {
  var n = 0;
  while (true) {
    yield n;
    n += 2;
  }
}

void main() {
  print(evenNumbers().take(5).toList());
}
output
[0, 2, 4, 6, 8]

take(5) asks for five values and then stops asking, so the while (true) never runs a sixth time. Calling toList() on the whole endless sequence would never finish.


5. async* — Values Over Time

Mark the function async* and return a Stream<T>. Now you may await between yields:

dart
Stream<int> countdown(int from) async* {
  for (var i = from; i >= 1; i--) {
    await Future.delayed(Duration(seconds: 1));
    yield i;
  }
}

Future<void> main() async {
  await for (var second in countdown(3)) {
    print(second);
  }
  print('Lift off!');
}
output
3
2
1
Lift off!

Each number appears one second after the previous one. No controller, no add, no close: the stream closes by itself when the function ends.


6. The Body Runs on Listen

An async* generator is lazy too. Creating the stream runs nothing; the body starts when someone listens:

dart
Stream<String> messages() async* {
  print('Generator started');
  yield 'Hi';
  yield 'Bye';
}

Future<void> main() async {
  var stream = messages();
  print('Stream created');
  await Future.delayed(Duration(seconds: 1));
  print('Now listening');
  await for (var message in stream) {
    print(message);
  }
}
output
Stream created
Now listening
Generator started
Hi
Bye

A whole second passed with the stream sitting idle. Generator started printed only once await for began listening.


7. yield* — Pass On Another Generator

yield* sends out every value of another stream (or, in a sync* generator, another Iterable) before moving on:

dart
Stream<String> morning() async* {
  yield 'Math';
  yield 'Science';
}

Stream<String> afternoon() async* {
  yield 'English';
  yield 'Art';
}

Stream<String> schoolDay() async* {
  yield* morning();
  yield 'Lunch';
  yield* afternoon();
}

Future<void> main() async {
  await for (var period in schoolDay()) {
    print(period);
  }
}
output
Math
Science
Lunch
English
Art

yield* lets you build a big generator out of small ones.


8. Cancellation Stops the Generator

When the listener stops — break out of await for, or cancel() a subscription — the generator stops at its current yield and runs any finally block:

dart
Stream<int> ticker() async* {
  var tick = 1;
  try {
    while (true) {
      await Future.delayed(Duration(milliseconds: 500));
      print('Generating $tick');
      yield tick++;
    }
  } finally {
    print('Ticker stopped');
  }
}

Future<void> main() async {
  await for (var tick in ticker()) {
    print('Got $tick');
    if (tick == 3) break;
  }
  print('Done');
}
output
Generating 1
Got 1
Generating 2
Got 2
Generating 3
Got 3
Ticker stopped
Done

The while (true) loop never produced a fourth tick. The same happens with listen (same loop, without the Generating line):

dart
Future<void> main() async {
  var subscription = ticker().listen((tick) => print('Tick $tick'));
  await Future.delayed(Duration(milliseconds: 1600));
  await subscription.cancel();
  print('Cancelled');
}
output
Tick 1
Tick 2
Tick 3
Ticker stopped
Cancelled

finally is the place to clean up — close a file, stop a timer — when nobody is listening any more.


9. Errors from a Generator

A throw inside an async* generator becomes an error event on the stream and ends it. Catch it with try around await for:

dart
Stream<int> marks() async* {
  yield 80;
  throw Exception('Server down');
}

Future<void> main() async {
  try {
    await for (var mark in marks()) {
      print(mark);
    }
  } catch (e) {
    print('Caught: $e');
  }
}
output
80
Caught: Exception: Server down

10. A Real-World Example

A shop's server sends products one page at a time. The generator fetches a page, yields its items, and asks for the next page only when the listener has used them all:

dart
Future<List<String>> fetchPage(int page) async {
  await Future.delayed(Duration(milliseconds: 500));
  print('Fetched page $page');
  var pages = [
    ['Pen', 'Notebook'],
    ['Bag', 'Ruler'],
    ['Eraser'],
  ];
  return page <= pages.length ? pages[page - 1] : [];
}

Stream<String> allProducts() async* {
  var page = 1;
  while (true) {
    var items = await fetchPage(page);
    if (items.isEmpty) return;
    for (var item in items) {
      yield item;
    }
    page++;
  }
}

Future<void> main() async {
  await for (var product in allProducts()) {
    print('  $product');
  }
  print('All products loaded');
}
output
Fetched page 1
  Pen
  Notebook
Fetched page 2
  Bag
  Ruler
Fetched page 3
  Eraser
Fetched page 4
All products loaded

The caller just sees one stream of products. A bare return; ends the generator — and the stream — once an empty page arrives. If the caller breaks early, later pages are never fetched at all.


Code Examples

Example 1 — A sync* Countdown

dart
Iterable<int> countdown(int from) sync* {
  for (var i = from; i >= 1; i--) {
    yield i;
  }
}

void main() {
  print(countdown(5).toList());
}
output
[5, 4, 3, 2, 1]

Example 2 — Squares on Demand

dart
Iterable<int> squares() sync* {
  var n = 1;
  while (true) {
    print('Computing $n');
    yield n * n;
    n++;
  }
}

void main() {
  print(squares().take(3).toList());
}
output
Computing 1
Computing 2
Computing 3
[1, 4, 9]

Example 3 — yield* in a sync* Generator

dart
Iterable<String> team() sync* {
  yield 'Ali';
  yield* ['Sara', 'Zayan'];
}

void main() {
  print(team().toList());
}
output
[Ali, Sara, Zayan]

Example 4 — A Simple Stream of Names

dart
Stream<String> names() async* {
  yield 'Ali';
  yield 'Sara';
}

Future<void> main() async {
  await for (var name in names()) {
    print('Welcome, $name');
  }
}
output
Welcome, Ali
Welcome, Sara

Example 5 — Loading Steps with await

dart
Stream<String> loading() async* {
  for (var step in ['Connecting', 'Downloading', 'Finished']) {
    await Future.delayed(Duration(milliseconds: 300));
    yield step;
  }
}

Future<void> main() async {
  await for (var step in loading()) {
    print(step);
  }
}
output
Connecting
Downloading
Finished

Example 6 — A Ticker Stopped by take

dart
Stream<int> ticker() async* {
  var tick = 1;
  while (true) {
    await Future.delayed(Duration(milliseconds: 200));
    yield tick++;
  }
}

Future<void> main() async {
  await for (var tick in ticker().take(3)) {
    print('Tick $tick');
  }
}
output
Tick 1
Tick 2
Tick 3

Example 7 — Student Marks Arriving One by One

dart
Stream<int> marks() async* {
  for (var mark in [72, 85, 90]) {
    await Future.delayed(Duration(milliseconds: 300));
    yield mark;
  }
}

Future<void> main() async {
  var total = 0;
  await for (var mark in marks()) {
    total += mark;
    print('Mark $mark, total $total');
  }
}
output
Mark 72, total 72
Mark 85, total 157
Mark 90, total 247

Example 8 — Shopping Total

dart
Stream<double> scanItems(List<double> prices) async* {
  for (var price in prices) {
    await Future.delayed(Duration(milliseconds: 200));
    yield price;
  }
}

Future<void> main() async {
  var total = 0.0;
  await for (var price in scanItems([250.0, 99.5, 40.0])) {
    total += price;
  }
  print('Total: $total');
}
output
Total: 389.5

Example 9 — Bank Balance with Interest

dart
Iterable<double> balanceByYear(double start) sync* {
  var balance = start;
  while (true) {
    balance = balance * 1.1;
    yield balance;
  }
}

void main() {
  for (var balance in balanceByYear(1000.0).take(3)) {
    print(balance.toStringAsFixed(2));
  }
}
output
1100.00
1210.00
1331.00

Example 10 — Temperature Sensor

dart
Stream<double> sensor() async* {
  var temperature = 26.0;
  try {
    while (true) {
      await Future.delayed(Duration(milliseconds: 300));
      temperature += 1.5;
      yield temperature;
    }
  } finally {
    print('Sensor off');
  }
}

Future<void> main() async {
  await for (var t in sensor()) {
    print('$t°C');
    if (t > 30) break;
  }
}
output
27.5°C
29.0°C
30.5°C
Sensor off

Common Beginner Mistakes

1. Using yield without sync* or async*

dart
// Iterable<int> countTo(int n) {
//   for (var i = 1; i <= n; i++) {
//     yield i; // ✗
//   }
// }
output
Error: 'yield' isn't a type.
    yield i;
    ^^^^^

Outside a generator, yield is not a keyword, so Dart misreads the line. Add sync* (or async*) after the parameters.

2. The wrong return type

dart
// List<int> countTo(int n) sync* { yield 1; } // ✗
// Future<int> score() async* { yield 1; } // ✗
output
Error: Functions marked 'sync*' must have a return type assignable to 'Iterable'.
Error: Functions marked 'async*' must have a return type assignable to 'Stream'.

sync* returns an Iterable, async* returns a Stream — never a List or a Future.

3. Returning a value from a generator

dart
// Stream<int> marks() async* {
//   return 90; // ✗
// }
output
Error: 'sync*' and 'async*' can't return a value.

Use yield 90;. A bare return; is allowed — it just ends the generator.

4. yield* with a list inside async*

dart
// Stream<int> more() async* {
//   yield* [1, 2]; // ✗
// }
output
Error: A value of type 'List<int>' can't be assigned to a variable of type 'Stream<int>'.

In async*, yield* needs a stream. Loop over the list and yield each item.

5. Expecting the generator to run without a listener

Calling countdown(3) and never listening prints nothing and waits for nothing — no error, just silence (Step 6). Use await for or listen.


Best Practices

  • Reach for async* when a stream comes from one piece of logic; keep StreamController for values pushed in from outside.
  • Use sync* for sequences you compute on the spot, async* when you need await.
  • Endless generators are fine — make sure the caller stops them with take or break.
  • Put clean-up in finally so it runs on cancellation too.
  • Combine small generators with yield* instead of copying their loops.

Mini Challenge

Sara's quiz app shows a 5-second timer. Write an async* generator timer(int seconds) that yields the seconds left, one per second (use 200 ms in testing), with a finally that prints Timer cleared. Sara answers when 3 seconds are left — stop listening there and print Answered!.

Try it yourself before opening the solution.

Show solution
dart
Stream<int> timer(int seconds) async* {
  try {
    for (var left = seconds; left >= 1; left--) {
      yield left;
      await Future.delayed(Duration(milliseconds: 200));
    }
  } finally {
    print('Timer cleared');
  }
}

Future<void> main() async {
  await for (var left in timer(5)) {
    print('$left s left');
    if (left == 3) break;
  }
  print('Answered!');
}
output
5 s left
4 s left
3 s left
Timer cleared
Answered!

How it works — break cancels the subscription, so the generator stops at its yield and its finally runs before Answered!.


Practice Questions

Beginner

  1. Write a sync* generator oddNumbers(int max) that yields 1, 3, 5, … up to max. Print oddNumbers(9).toList().

  2. Write an async* generator countUp(int n) that yields 1 to n, waiting 500 ms before each. Print every value with await for.

  3. Predict the output of this program, then run it:

    dart
    Iterable<String> letters() sync* {
      print('start');
      yield 'A';
      print('middle');
      yield 'B';
    }
    
    void main() {
      var result = letters();
      print('before');
      print(result.first);
    }

Application

  1. Write an endless sync* generator multiplesOf(int n) and print the first four multiples of 7.

  2. Write an async* generator that takes ['Ali', 'Sara', 'Zayan'] and yields Hello, <name> every 300 ms.

  3. Write vegetables() and fruits() as async* generators, then a shoppingList() generator that uses yield* to send out both lists in that order.

  4. This code does not compile. Fix it so it prints [10, 20]:

    dart
    // List<int> scores() sync* {
    //   yield 10;
    //   yield 20;
    // }
    //
    // void main() => print(scores().toList());

Challenge

  1. Write an endless async* clock() that yields 1, 2, 3, … every 200 ms and prints Clock stopped in a finally. Listen with listen, cancel after about 900 ms, then print Bye.
  2. Write Stream<double> balances(double start, List<double> changes) that yields the balance after each change, 200 ms apart. With 1000.0 and [500.0, -200.0, -50.0], print Balance: … each time.
  3. A server returns student marks in pages: page 1 [70, 80], page 2 [90], then an empty page. Write a paginated async* generator that yields each mark, and print the total and average.

Practice Answers

Question 1 — Solution

dart
Iterable<int> oddNumbers(int max) sync* {
  for (var i = 1; i <= max; i += 2) {
    yield i;
  }
}

void main() {
  print(oddNumbers(9).toList());
}
output
[1, 3, 5, 7, 9]

Question 2 — Solution

dart
Stream<int> countUp(int n) async* {
  for (var i = 1; i <= n; i++) {
    await Future.delayed(Duration(milliseconds: 500));
    yield i;
  }
}

Future<void> main() async {
  await for (var value in countUp(3)) {
    print(value);
  }
}
output
1
2
3

Question 3 — Solution

output
before
start
A

The body starts only when first asks for a value, and stops at the first yield — middle never prints.

Question 4 — Solution

dart
Iterable<int> multiplesOf(int n) sync* {
  var value = n;
  while (true) {
    yield value;
    value += n;
  }
}

void main() {
  print(multiplesOf(7).take(4).toList());
}
output
[7, 14, 21, 28]

Question 5 — Solution

dart
Stream<String> greetings(List<String> names) async* {
  for (var name in names) {
    await Future.delayed(Duration(milliseconds: 300));
    yield 'Hello, $name';
  }
}

Future<void> main() async {
  await for (var line in greetings(['Ali', 'Sara', 'Zayan'])) {
    print(line);
  }
}
output
Hello, Ali
Hello, Sara
Hello, Zayan

Question 6 — Solution

dart
Stream<String> vegetables() async* {
  yield 'Potato';
  yield 'Onion';
}

Stream<String> fruits() async* {
  yield 'Apple';
  yield 'Mango';
}

Stream<String> shoppingList() async* {
  yield* vegetables();
  yield* fruits();
}

Future<void> main() async {
  await for (var item in shoppingList()) {
    print(item);
  }
}
output
Potato
Onion
Apple
Mango

Question 7 — Solution

A sync* generator must return an Iterable:

dart
Iterable<int> scores() sync* {
  yield 10;
  yield 20;
}

void main() => print(scores().toList());
output
[10, 20]

Question 8 — Solution

dart
Stream<int> clock() async* {
  var second = 1;
  try {
    while (true) {
      await Future.delayed(Duration(milliseconds: 200));
      yield second++;
    }
  } finally {
    print('Clock stopped');
  }
}

Future<void> main() async {
  var subscription = clock().listen((s) => print(s));
  await Future.delayed(Duration(milliseconds: 900));
  await subscription.cancel();
  print('Bye');
}
output
1
2
3
4
Clock stopped
Bye

Question 9 — Solution

dart
Stream<double> balances(double start, List<double> changes) async* {
  var balance = start;
  for (var change in changes) {
    await Future.delayed(Duration(milliseconds: 200));
    balance += change;
    yield balance;
  }
}

Future<void> main() async {
  await for (var b in balances(1000.0, [500.0, -200.0, -50.0])) {
    print('Balance: $b');
  }
}
output
Balance: 1500.0
Balance: 1300.0
Balance: 1250.0

Question 10 — Solution

dart
Future<List<int>> fetchMarks(int page) async {
  await Future.delayed(Duration(milliseconds: 300));
  var pages = [
    [70, 80],
    [90],
  ];
  return page <= pages.length ? pages[page - 1] : [];
}

Stream<int> allMarks() async* {
  var page = 1;
  while (true) {
    var marks = await fetchMarks(page);
    if (marks.isEmpty) return;
    for (var mark in marks) {
      yield mark;
    }
    page++;
  }
}

Future<void> main() async {
  var total = 0;
  var count = 0;
  await for (var mark in allMarks()) {
    total += mark;
    count++;
  }
  print('Total: $total');
  print('Average: ${total / count}');
}
output
Total: 240
Average: 80.0

Quick Revision

  1. What does yield do that return does not?
  2. Which return type goes with sync*, and which with async*?
  3. When does a generator's body start running?
  4. Why can a generator safely contain while (true)?
  5. What does yield* do?
  6. What happens to an async* generator when its listener cancels?
  7. How does an async* generator close its stream?

Key Takeaways

  • A generator produces many values one at a time with yield.
  • sync* returns an Iterable; async* returns a Stream and may use await.
  • Generators are lazy: the body runs only when values are requested or the stream is listened to, pausing at each yield.
  • Endless generators are safe when the caller stops with take or break.
  • yield* passes on every value of another generator.
  • Cancelling — break in await for or cancel() — stops the generator and runs finally.
  • The stream closes when the function ends; a throw becomes an error event.
  • async* turns countdowns, tickers, and paginated loading into plain loops.

What's Next?

Lecture 097 — Dart Concurrency Model. This lecture closes Asynchronous Programming. All our futures, streams, and generators shared one thread and its event loop — great for waiting, useless for heavy calculation. Next we will see how Dart runs code truly at the same time, and why it does so without shared memory.


Video lecture

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