On this page22 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. A Function That Produces Many Values
- 2. sync* and yield
- 3. Generators Are Lazy
- 4. An Endless Generator
- 5. async* — Values Over Time
- 6. The Body Runs on Listen
- 7. yield* — Pass On Another Generator
- 8. Cancellation Stops the Generator
- 9. Errors from a Generator
- 10. 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 96 — Async Generators
| Category | 15 — Asynchronous Programming |
| Lecture | 096 of 140 |
| Estimated video length | 10 min |
| Target | Dart 3.12.2 |
| Level | Intermediate |
A normal function hands back one value and stops. A generator hands back values one at a time with
yield— and anasync*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, andtoList()(Lectures 043 and 046)try/catch/finally(Lecture 084)Future.delayed,async, andawait(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 anIterablewithyield - Explain why a generator's body runs only when someone asks for values
- Write an
async*generator that returns aStream - Use
awaitinside anasync*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:
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');
}
}(1, 2, 3)
Got 1
Got 2
Got 3yield 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:
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');
}
}Nothing has run yet
Generator started
Hello, Ali
After Ali
Hello, Sara
After Sara
Hello, ZayanThe 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:
Iterable<int> evenNumbers() sync* {
var n = 0;
while (true) {
yield n;
n += 2;
}
}
void main() {
print(evenNumbers().take(5).toList());
}[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:
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!');
}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:
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);
}
}Stream created
Now listening
Generator started
Hi
ByeA 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:
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);
}
}Math
Science
Lunch
English
Artyield* 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:
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');
}Generating 1
Got 1
Generating 2
Got 2
Generating 3
Got 3
Ticker stopped
DoneThe while (true) loop never produced a fourth tick. The same happens with listen (same
loop, without the Generating line):
Future<void> main() async {
var subscription = ticker().listen((tick) => print('Tick $tick'));
await Future.delayed(Duration(milliseconds: 1600));
await subscription.cancel();
print('Cancelled');
}Tick 1
Tick 2
Tick 3
Ticker stopped
Cancelledfinally 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:
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');
}
}80
Caught: Exception: Server down10. 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:
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');
}Fetched page 1
Pen
Notebook
Fetched page 2
Bag
Ruler
Fetched page 3
Eraser
Fetched page 4
All products loadedThe 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
Iterable<int> countdown(int from) sync* {
for (var i = from; i >= 1; i--) {
yield i;
}
}
void main() {
print(countdown(5).toList());
}[5, 4, 3, 2, 1]Example 2 — Squares on Demand
Iterable<int> squares() sync* {
var n = 1;
while (true) {
print('Computing $n');
yield n * n;
n++;
}
}
void main() {
print(squares().take(3).toList());
}Computing 1
Computing 2
Computing 3
[1, 4, 9]Example 3 — yield* in a sync* Generator
Iterable<String> team() sync* {
yield 'Ali';
yield* ['Sara', 'Zayan'];
}
void main() {
print(team().toList());
}[Ali, Sara, Zayan]Example 4 — A Simple Stream of Names
Stream<String> names() async* {
yield 'Ali';
yield 'Sara';
}
Future<void> main() async {
await for (var name in names()) {
print('Welcome, $name');
}
}Welcome, Ali
Welcome, SaraExample 5 — Loading Steps with await
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);
}
}Connecting
Downloading
FinishedExample 6 — A Ticker Stopped by take
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');
}
}Tick 1
Tick 2
Tick 3Example 7 — Student Marks Arriving One by One
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');
}
}Mark 72, total 72
Mark 85, total 157
Mark 90, total 247Example 8 — Shopping Total
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');
}Total: 389.5Example 9 — Bank Balance with Interest
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));
}
}1100.00
1210.00
1331.00Example 10 — Temperature Sensor
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;
}
}27.5°C
29.0°C
30.5°C
Sensor offCommon Beginner Mistakes
1. Using yield without sync* or async*
// Iterable<int> countTo(int n) {
// for (var i = 1; i <= n; i++) {
// yield i; // ✗
// }
// }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
// List<int> countTo(int n) sync* { yield 1; } // ✗
// Future<int> score() async* { yield 1; } // ✗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
// Stream<int> marks() async* {
// return 90; // ✗
// }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*
// Stream<int> more() async* {
// yield* [1, 2]; // ✗
// }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; keepStreamControllerfor values pushed in from outside. - Use
sync*for sequences you compute on the spot,async*when you needawait. - Endless generators are fine — make sure the caller stops them with
takeorbreak. - Put clean-up in
finallyso 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
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!');
}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
-
Write a
sync*generatoroddNumbers(int max)that yields 1, 3, 5, … up tomax. PrintoddNumbers(9).toList(). -
Write an
async*generatorcountUp(int n)that yields 1 ton, waiting 500 ms before each. Print every value withawait for. -
Predict the output of this program, then run it:
Iterable<String> letters() sync* { print('start'); yield 'A'; print('middle'); yield 'B'; } void main() { var result = letters(); print('before'); print(result.first); }
Application
-
Write an endless
sync*generatormultiplesOf(int n)and print the first four multiples of 7. -
Write an
async*generator that takes['Ali', 'Sara', 'Zayan']and yieldsHello, <name>every 300 ms. -
Write
vegetables()andfruits()asasync*generators, then ashoppingList()generator that usesyield*to send out both lists in that order. -
This code does not compile. Fix it so it prints
[10, 20]:// List<int> scores() sync* { // yield 10; // yield 20; // } // // void main() => print(scores().toList());
Challenge
- Write an endless
async*clock()that yields 1, 2, 3, … every 200 ms and printsClock stoppedin afinally. Listen withlisten, cancel after about 900 ms, then printBye. - Write
Stream<double> balances(double start, List<double> changes)that yields the balance after each change, 200 ms apart. With1000.0and[500.0, -200.0, -50.0], printBalance: …each time. - A server returns student marks in pages: page 1
[70, 80], page 2[90], then an empty page. Write a paginatedasync*generator that yields each mark, and print the total and average.
Practice Answers
Question 1 — Solution
Iterable<int> oddNumbers(int max) sync* {
for (var i = 1; i <= max; i += 2) {
yield i;
}
}
void main() {
print(oddNumbers(9).toList());
}[1, 3, 5, 7, 9]Question 2 — Solution
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);
}
}1
2
3Question 3 — Solution
before
start
AThe body starts only when first asks for a value, and stops at the first yield —
middle never prints.
Question 4 — Solution
Iterable<int> multiplesOf(int n) sync* {
var value = n;
while (true) {
yield value;
value += n;
}
}
void main() {
print(multiplesOf(7).take(4).toList());
}[7, 14, 21, 28]Question 5 — Solution
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);
}
}Hello, Ali
Hello, Sara
Hello, ZayanQuestion 6 — Solution
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);
}
}Potato
Onion
Apple
MangoQuestion 7 — Solution
A sync* generator must return an Iterable:
Iterable<int> scores() sync* {
yield 10;
yield 20;
}
void main() => print(scores().toList());[10, 20]Question 8 — Solution
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');
}1
2
3
4
Clock stopped
ByeQuestion 9 — Solution
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');
}
}Balance: 1500.0
Balance: 1300.0
Balance: 1250.0Question 10 — Solution
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}');
}Total: 240
Average: 80.0Quick Revision
- What does
yielddo thatreturndoes not? - Which return type goes with
sync*, and which withasync*? - When does a generator's body start running?
- Why can a generator safely contain
while (true)? - What does
yield*do? - What happens to an
async*generator when its listener cancels? - How does an
async*generator close its stream?
Key Takeaways
- A generator produces many values one at a time with
yield. sync*returns anIterable;async*returns aStreamand may useawait.- 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
takeorbreak. yield*passes on every value of another generator.- Cancelling —
breakinawait fororcancel()— stops the generator and runsfinally. - The stream closes when the function ends; a
throwbecomes 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.