On this page23 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. One Value or Many?
- 2. await for Waits Until the Stream Is Done
- 3. Values Over Time: Stream.periodic and take
- 4. One-Off Streams: Stream.value and Stream.error
- 5. Errors Stop an await for Loop
- 6. Listening with listen
- 7. onError and onDone
- 8. Stopping Early: cancel
- 9. pause and resume
- 10. Most Streams Allow One Listener
- 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 92 — Streams
| Category | 15 — Asynchronous Programming |
| Lecture | 092 of 140 |
| Estimated video length | 12 min |
| Target | Dart 3.12.2 |
| Level | Intermediate |
A
Futurehands you one value and is finished. AStreamkeeps delivering values — one, then another, then another — until it says it is done.
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. Long error traces are shortened to their first lines.
Prerequisites
Students should already understand, from Lectures 084–091:
try,catch, andonfrom the exception lecturesFuture,Future.delayed, andFuture.value(Lecture 088)asyncandawait(Lecture 089)- catching errors around
await(Lecture 090) Durationand timing work withStopwatch(Lecture 091)
Connection With Previous Lectures
Every async tool so far gives back one result:
var marks = await loadMarks(); // one value, then the Future is finishedBut a chat app receives message after message. A thermometer reports a new temperature every
second. A bank account sees payment after payment. A Future cannot do this — it completes
once. We need a value that can arrive many times, spread out over time, and a way to
react to each one as it comes.
Learning Objectives
By the end of this lecture you will be able to:
- Explain the difference between a
Future(one value) and aStream(many values) - Create streams with
Stream.fromIterable,Stream.periodic,Stream.value, andStream.error - Limit a never-ending stream with
take - Read every value of a stream with
await for - Handle errors in a stream with
try/catch - Listen to a stream with
listen, usingonData,onError, andonDone - Pause, resume, and cancel a
StreamSubscription - Recognize the "already been listened to" error
1. One Value or Many?
Analogy — a parcel versus a newspaper subscription. A parcel arrives once and the delivery is over: that is a
Future. A subscription brings a new paper every morning until it ends: that is aStream.
await takes the one value out of a future. await for takes each value out of a stream:
void main() async {
var marks = await Future.value(88);
print('Future: $marks');
await for (var mark in Stream.fromIterable([88, 92, 75])) {
print('Stream: $mark');
}
}Future: 88
Stream: 88
Stream: 92
Stream: 75A Stream<int> is a source of int values that arrive one at a time. When there are no more
values, the stream is done.
2. await for Waits Until the Stream Is Done
The body of await for runs once for each value. The line after the loop runs only when
the stream is done:
void main() async {
var students = Stream.fromIterable(['Ali', 'Sara', 'Zayan']);
await for (var name in students) {
print('Hello, $name');
}
print('Everyone greeted');
}Hello, Ali
Hello, Sara
Hello, Zayan
Everyone greetedLike await, await for is allowed only inside an async function, and it pauses only that
function — the rest of the program keeps running.
3. Values Over Time: Stream.periodic and take
Stream.fromIterable delivers values from a list you already have. Stream.periodic creates
a new value every time a Duration passes, by calling your function with a counter that
starts at 0:
void main() async {
var stopwatch = Stopwatch()..start();
var ticks = Stream.periodic(Duration(seconds: 1), (count) => count + 1);
await for (var tick in ticks.take(3)) {
print('Tick $tick at ${stopwatch.elapsedMilliseconds} ms');
}
print('Done');
}Tick 1 at 1004 ms
Tick 2 at 2002 ms
Tick 3 at 3002 ms
DoneA periodic stream never ends on its own. .take(3) gives a new stream that delivers the
first 3 values and is then done. Without it, the loop would run forever. We will study
take and its relatives in Lecture 095 — Stream Transformations.
4. One-Off Streams: Stream.value and Stream.error
Just as Future.value and Future.error make ready-made futures, Stream.value makes a
stream with one value, and Stream.error a stream with one error:
void main() async {
await for (var balance in Stream.value(2500.0)) {
print('Balance: $balance');
}
try {
await for (var balance in Stream<double>.error(Exception('Bank offline'))) {
print('Balance: $balance');
}
} catch (e) {
print('Caught: $e');
}
}Balance: 2500.0
Caught: Exception: Bank offlineBoth are done right after their single event. They are handy for tests and for functions that must return a stream but already know the answer.
5. Errors Stop an await for Loop
A stream can deliver errors as well as values. Inside await for, an error is thrown at
the loop, so try/catch around the loop catches it — and the loop stops:
void main() async {
var readings = Stream.periodic(Duration(milliseconds: 500), (count) {
if (count == 2) throw Exception('Sensor glitch');
return 20 + count;
}).take(4);
try {
await for (var temp in readings) {
print('Temperature: $temp');
}
} catch (e) {
print('Caught: $e');
}
}Temperature: 20
Temperature: 21
Caught: Exception: Sensor glitchThrowing inside the Stream.periodic function makes that event an error instead of a value.
The readings after the error are never seen. Without the try/catch, the program would
crash, just like an unhandled future error.
6. Listening with listen
await for is not the only way to read a stream. listen registers a function that runs for
each value — the onData handler — and does not wait:
void main() {
var prices = Stream.fromIterable([250, 100, 75]);
prices.listen((price) => print('Price: $price'));
print('Listening started');
}Listening started
Price: 250
Price: 100
Price: 75Listening started prints first: listen only sets up the handler, and the values arrive
later from the event loop. This is the stream version of .then from Lecture 088.
7. onError and onDone
listen takes two more named handlers. onError runs for each error, and onDone runs once
when the stream is done (same readings as Step 5):
void main() {
readings.listen(
(temp) => print('Temperature: $temp'),
onError: (e) => print('Error: $e'),
onDone: () => print('No more readings'),
);
}Temperature: 20
Temperature: 21
Error: Exception: Sensor glitch
Temperature: 23
Temperature: 24
No more readingsUnlike await for, listen keeps going after an error — readings 23 and 24 still
arrive. Notice there are four temperatures plus the error: take(4) counts values only,
so an error does not use up one of the four. Pass cancelOnError: true if you want listen
to stop at the first error instead.
8. Stopping Early: cancel
listen returns a StreamSubscription — your connection to the stream. Calling
cancel() on it stops the values:
void main() async {
var ticks = Stream.periodic(Duration(seconds: 1), (count) => count);
var subscription = ticks.listen((tick) => print('Tick $tick'));
await Future.delayed(Duration(milliseconds: 3500));
await subscription.cancel();
print('Cancelled');
}Tick 0
Tick 1
Tick 2
CancelledThis periodic stream has no take, yet the program ends: once cancelled, no more ticks are
scheduled. cancel() returns a Future, so we await it. After a cancel, onDone is
not called — the stream did not finish, you left it.
9. pause and resume
A subscription can also take a break. While paused, no values are delivered; after
resume(), they continue:
void main() async {
var ticks = Stream.periodic(
Duration(milliseconds: 500),
(count) => count + 1,
).take(4);
var subscription = ticks.listen(
(tick) => print('Tick $tick'),
onDone: () => print('Done'),
);
await Future.delayed(Duration(milliseconds: 1200));
subscription.pause();
print('Paused');
await Future.delayed(Duration(seconds: 2));
print('Resuming');
subscription.resume();
}Tick 1
Tick 2
Paused
Resuming
Tick 3
Tick 4
DoneNothing printed during the 2-second pause, and no tick was lost — the stream simply waited.
10. Most Streams Allow One Listener
A stream like Stream.periodic can be listened to only once. A second listen or
await for on the same stream fails:
void main() async {
var ticks = Stream.periodic(Duration(milliseconds: 100), (c) => c).take(2);
await for (var tick in ticks) {
print(tick);
}
// await for (var tick in ticks) { ... } // ✗ second listen
}0
1With the second loop uncommented:
Unhandled exception:
Bad state: Stream has already been listened to.These are single-subscription streams: one listener, from start to finish. Create a fresh
stream for each listener. (Stream.fromIterable is an exception — each listener simply
replays the list.) Streams that many listeners can share at once are the topic of Lecture
094 — Broadcast Streams.
11. A Real-World Example
Sara's bank account receives a payment every half second. Payment number 2 (counting from 0) is a declined card and shows up as an error. When the day's payments are done, the final balance is printed:
void main() {
var amounts = [500.0, -200.0, -900.0, -150.0];
var balance = 1000.0;
var payments = Stream.periodic(Duration(milliseconds: 500), (i) {
if (i == 2) throw Exception('Card declined: ${amounts[i]}');
return amounts[i];
}).take(3); // 3 payments go through; the error is not counted
payments.listen(
(amount) {
balance += amount;
print('Payment $amount → balance $balance');
},
onError: (e) => print('Skipped: $e'),
onDone: () => print('Day closed. Final balance: $balance'),
);
}Payment 500.0 → balance 1500.0
Payment -200.0 → balance 1300.0
Skipped: Exception: Card declined: -900.0
Payment -150.0 → balance 1150.0
Day closed. Final balance: 1150.0listen suits this job: one failed payment should not stop the rest. take(3) matters: with
take(4), the stream would wait for a fourth value and ask for amounts[4], which does not
exist (see Common Mistake 4). Here the values come from a list, but in a real app they would
come from a server — and the code reading them would stay the same. Building such a stream yourself is the topic of Lecture 093.
Code Examples
Example 1 — await for Over a List
void main() async {
await for (var n in Stream.fromIterable([1, 2, 3])) {
print(n * 10);
}
}10
20
30Example 2 — A Countdown with Stream.periodic
void main() async {
var countdown = Stream.periodic(Duration(seconds: 1), (i) => 3 - i).take(3);
await for (var n in countdown) {
print(n);
}
print('Go!');
}3
2
1
Go!Example 3 — Stream.value
void main() async {
await for (var name in Stream.value('Zayan')) {
print('Only value: $name');
}
print('Stream done');
}Only value: Zayan
Stream doneExample 4 — listen with All Three Handlers
void main() {
Stream.fromIterable(['Ali', 'Sara']).listen(
(name) => print('Got $name'),
onError: (e) => print('Error: $e'),
onDone: () => print('Done'),
);
}Got Ali
Got Sara
DoneExample 5 — Leaving await for with break
void main() async {
var ticks = Stream.periodic(Duration(milliseconds: 200), (i) => i);
await for (var tick in ticks) {
if (tick == 3) break;
print('Tick $tick');
}
print('Stopped');
}Tick 0
Tick 1
Tick 2
Stoppedbreak leaves the loop and cancels the subscription, so even an endless stream stops.
Example 6 — Catching Stream.error with onError
void main() {
Stream<int>.error(Exception('No data')).listen(
(value) => print('Value: $value'),
onError: (e) => print('Handled: $e'),
onDone: () => print('Done'),
);
}Handled: Exception: No data
DoneExample 7 — Student Marks
void main() async {
var marks = Stream.fromIterable([90, 85, 78]);
var total = 0;
await for (var mark in marks) {
total += mark;
print('Received $mark, total so far $total');
}
print('Final total: $total');
}Received 90, total so far 90
Received 85, total so far 175
Received 78, total so far 253
Final total: 253Example 8 — Shopping Total
void main() {
var prices = [1500.0, 50.0, 300.0];
var total = 0.0;
Stream.periodic(Duration(milliseconds: 300), (i) => prices[i])
.take(prices.length)
.listen(
(price) => total += price,
onDone: () => print('Cart total: $total'),
);
}Cart total: 1850.0Example 9 — Bank Balance with a Pause
void main() async {
var deposits = Stream.periodic(
Duration(milliseconds: 400),
(i) => 100.0,
).take(3);
var balance = 0.0;
var subscription = deposits.listen((d) {
balance += d;
print('Balance: $balance');
}, onDone: () => print('All deposits in'));
subscription.pause();
print('Checking account...');
await Future.delayed(Duration(seconds: 1));
subscription.resume();
}Checking account...
Balance: 100.0
Balance: 200.0
Balance: 300.0
All deposits inExample 10 — Temperature Readings with a Glitch
void main() async {
var readings = Stream.periodic(Duration(milliseconds: 300), (i) {
if (i == 1) throw Exception('Sensor offline');
return 24.0 + i;
}).take(3);
try {
await for (var temp in readings) {
print('Temperature: $temp°C');
}
} catch (e) {
print('Stopped: $e');
}
}Temperature: 24.0°C
Stopped: Exception: Sensor offlineCommon Beginner Mistakes
1. Using await instead of await for
void main() async {
var marks = Stream.fromIterable([88, 92]);
var mark = await marks; // ✗ not a Future
print(mark);
}Instance of '_MultiStream<int>'No error appears, but await on something that is not a Future just hands it straight
back — you get the stream object, not a mark. To read the values, loop with await for.
2. await for outside an async function
void main() {
// await for (var n in Stream.fromIterable([1, 2])) {} // ✗
}Error: The asynchronous for-in can only be used in functions marked with 'async' or 'async*'.Add async to the function. (async* is the topic of Lecture 096 — Async Generators.)
3. Forgetting take on Stream.periodic
await for (var tick in Stream.periodic(Duration(seconds: 1), (i) => i)) {
print(tick); // runs forever
}
print('Done'); // never reachedNo error appears — the program just never ends. Limit the stream with take, leave the loop
with break, or cancel() the subscription.
4. Counting errors in take
Here is Step 11 with take(amounts.length) instead of take(3):
var payments = Stream.periodic(Duration(milliseconds: 500), (i) {
if (i == 2) throw Exception('Card declined');
return amounts[i];
}).take(amounts.length); // ✗ expects 4 values, but only 3 existSkipped: Exception: Card declined
Skipped: RangeError (length): Invalid value: Not in inclusive range 0..3: 4
Skipped: RangeError (length): Invalid value: Not in inclusive range 0..3: 5Errors do not count toward take, so the stream keeps asking for indexes past the end of
the list — and every failure is just another error that does not count. The program never
ends.
5. Listening to the same stream twice
void main() {
var ticks = Stream.periodic(Duration(milliseconds: 100), (c) => c).take(2);
ticks.listen(print);
ticks.listen(print); // ✗
}Unhandled exception:
Bad state: Stream has already been listened to.Create a new stream for the second listener, or wait for Lecture 094 — Broadcast Streams.
6. Expecting await for to continue after an error
As Step 5 showed, the first error ends the loop. If later values still matter, use listen
with onError.
Best Practices
- Use a
Futurefor one result and aStreamfor a series of results. - Prefer
await forwhen values must be handled in order and the code after should wait for the end. - Use
listenwhen the program should carry on while values arrive, or when one error should not stop the rest. - Always give
listenanonErrorhandler when the stream can fail; wrapawait forintry/catch. - Give every endless stream an end:
take,break, orcancel(). - Keep the
StreamSubscriptionif you may need to pause, resume, or cancel later.
Mini Challenge
Ali's quiz app sends a new score every 300 ms: 7, 9, 4, 10. Read the scores with
await for, print each one, and when the stream is done print the highest score.
Try it yourself before opening the solution.
Show solution
void main() async {
var scores = [7, 9, 4, 10];
var stream = Stream.periodic(
Duration(milliseconds: 300),
(i) => scores[i],
).take(scores.length);
var best = 0;
await for (var score in stream) {
print('Score: $score');
if (score > best) best = score;
}
print('Best score: $best');
}Score: 7
Score: 9
Score: 4
Score: 10
Best score: 10How it works — take(scores.length) ends the stream after four values, so the loop
finishes and the line after it prints the best score.
Practice Questions
Solve each one before looking at the answers.
Beginner
- Use
Stream.fromIterableandawait forto print'Ali','Sara', and'Zayan'. - Use
Stream.periodicandtaketo printTick 1,Tick 2,Tick 3, one per second. - Use
listenwithonDoneto print the numbers5and6, thenFinished.
Application
-
What does this print, and in what order?
void main() { Stream.value('B').listen(print); print('A'); } -
Read
Stream<String>.error(Exception('Offline'))withawait forand printCaught: Exception: Offline. -
Add up the prices
200.0,350.0, and50.0from a stream withlisten, and print the total inonDone. -
Listen to a 1-second periodic stream and cancel the subscription after about 2.5 seconds. Print
Stoppedafter cancelling.
Challenge
- A periodic stream throws on index 1 and is limited with
take(4). Print every value and every error, thenDone, so that no readings are lost. - Listen to a 300 ms periodic stream of 4 values, pause it right away, and resume it after
1 second. Print
PausedandResumedat the right moments. - A stream delivers bank deposits
100.0,250.0,400.0,50.0. Usingawait for, stop reading as soon as the balance reaches at least700.0and print the balance.
Practice Answers
Question 1 — Solution
void main() async {
await for (var name in Stream.fromIterable(['Ali', 'Sara', 'Zayan'])) {
print(name);
}
}Ali
Sara
ZayanQuestion 2 — Solution
void main() async {
var ticks = Stream.periodic(Duration(seconds: 1), (i) => i + 1).take(3);
await for (var tick in ticks) {
print('Tick $tick');
}
}Tick 1
Tick 2
Tick 3Question 3 — Solution
void main() {
Stream.fromIterable([5, 6]).listen(print, onDone: () => print('Finished'));
}5
6
FinishedQuestion 4 — Solution
A
Blisten only registers the handler; the value arrives after main's own code has run.
Question 5 — Solution
void main() async {
try {
await for (var text in Stream<String>.error(Exception('Offline'))) {
print(text);
}
} catch (e) {
print('Caught: $e');
}
}Caught: Exception: OfflineQuestion 6 — Solution
void main() {
var total = 0.0;
var prices = Stream.fromIterable([200.0, 350.0, 50.0]);
prices.listen(
(price) => total += price,
onDone: () => print('Total: $total'),
);
}Total: 600.0Question 7 — Solution
void main() async {
var ticks = Stream.periodic(Duration(seconds: 1), (i) => i);
var subscription = ticks.listen((tick) => print('Tick $tick'));
await Future.delayed(Duration(milliseconds: 2500));
await subscription.cancel();
print('Stopped');
}Tick 0
Tick 1
StoppedQuestion 8 — Solution
void main() {
var readings = Stream.periodic(Duration(milliseconds: 300), (i) {
if (i == 1) throw Exception('Bad reading');
return 20 + i;
}).take(4);
readings.listen(
(value) => print('Reading: $value'),
onError: (e) => print('Error: $e'),
onDone: () => print('Done'),
);
}Reading: 20
Error: Exception: Bad reading
Reading: 22
Reading: 23
Reading: 24
Donetake(4) still delivers four readings because the error does not count. listen with
onError keeps going after the error; await for would have stopped.
Question 9 — Solution
void main() async {
var values = Stream.periodic(
Duration(milliseconds: 300),
(i) => i + 1,
).take(4);
var subscription = values.listen(print);
subscription.pause();
print('Paused');
await Future.delayed(Duration(seconds: 1));
print('Resumed');
subscription.resume();
}Paused
Resumed
1
2
3
4Question 10 — Solution
void main() async {
var deposits = Stream.fromIterable([100.0, 250.0, 400.0, 50.0]);
var balance = 0.0;
await for (var deposit in deposits) {
balance += deposit;
if (balance >= 700.0) break;
}
print('Balance: $balance');
}Balance: 750.0break leaves the loop after the third deposit; the last one is never read.
Quick Revision
- How many values does a
Futuredeliver? AStream? - When does the line after an
await forloop run? - Why does
Stream.periodicusually needtake? - What happens to an
await forloop when the stream delivers an error? - What happens to
listenwhen the stream delivers an error? - What do
onData,onError, andonDoneeach handle? - What does
listenreturn, and what can you do with it? - What error appears when you listen to a single-subscription stream twice?
Key Takeaways
- A
Futuredelivers one value; aStreamdelivers many values over time, then is done. - Create streams with
Stream.fromIterable,Stream.periodic,Stream.value, andStream.error. Stream.periodicnever ends on its own — limit it withtake, which counts values only.await forhandles each value in order; the code after it runs when the stream is done.- An error ends an
await forloop and is caught withtry/catcharound it. listentakesonData,onError, andonDone, and keeps going after errors.listenreturns aStreamSubscriptionyou canpause,resume, andcancel.- Most streams allow only one listener: a second gives
Bad state: Stream has already been listened to.
What's Next?
Lecture 093 — Stream Controllers. Every stream in this lecture was built from a list or a
timer. Real streams carry events nobody can predict: a message arrives, a button is pressed, a
payment comes in. Next we will use a StreamController to create our own stream and add
values, errors, and the "done" signal to it whenever we choose.