Watch the video (12 min)
On this page23 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. One Value or Many?
  5. 2. await for Waits Until the Stream Is Done
  6. 3. Values Over Time: Stream.periodic and take
  7. 4. One-Off Streams: Stream.value and Stream.error
  8. 5. Errors Stop an await for Loop
  9. 6. Listening with listen
  10. 7. onError and onDone
  11. 8. Stopping Early: cancel
  12. 9. pause and resume
  13. 10. Most Streams Allow One Listener
  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 92 — Streams

Category15 — Asynchronous Programming
Lecture092 of 140
Estimated video length12 min
TargetDart 3.12.2
LevelIntermediate

A Future hands you one value and is finished. A Stream keeps 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, and on from the exception lectures
  • Future, Future.delayed, and Future.value (Lecture 088)
  • async and await (Lecture 089)
  • catching errors around await (Lecture 090)
  • Duration and timing work with Stopwatch (Lecture 091)

Connection With Previous Lectures

Every async tool so far gives back one result:

dart
var marks = await loadMarks(); // one value, then the Future is finished

But 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 a Stream (many values)
  • Create streams with Stream.fromIterable, Stream.periodic, Stream.value, and Stream.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, using onData, onError, and onDone
  • 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 a Stream.

await takes the one value out of a future. await for takes each value out of a stream:

dart
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');
  }
}
output
Future: 88
Stream: 88
Stream: 92
Stream: 75

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

dart
void main() async {
  var students = Stream.fromIterable(['Ali', 'Sara', 'Zayan']);
  await for (var name in students) {
    print('Hello, $name');
  }
  print('Everyone greeted');
}
output
Hello, Ali
Hello, Sara
Hello, Zayan
Everyone greeted

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

dart
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');
}
output
Tick 1 at 1004 ms
Tick 2 at 2002 ms
Tick 3 at 3002 ms
Done

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

dart
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');
  }
}
output
Balance: 2500.0
Caught: Exception: Bank offline

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

dart
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');
  }
}
output
Temperature: 20
Temperature: 21
Caught: Exception: Sensor glitch

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

dart
void main() {
  var prices = Stream.fromIterable([250, 100, 75]);
  prices.listen((price) => print('Price: $price'));
  print('Listening started');
}
output
Listening started
Price: 250
Price: 100
Price: 75

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

dart
void main() {
  readings.listen(
    (temp) => print('Temperature: $temp'),
    onError: (e) => print('Error: $e'),
    onDone: () => print('No more readings'),
  );
}
output
Temperature: 20
Temperature: 21
Error: Exception: Sensor glitch
Temperature: 23
Temperature: 24
No more readings

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

dart
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');
}
output
Tick 0
Tick 1
Tick 2
Cancelled

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

dart
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();
}
output
Tick 1
Tick 2
Paused
Resuming
Tick 3
Tick 4
Done

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

dart
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
}
output
0
1

With the second loop uncommented:

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

dart
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'),
  );
}
output
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.0

listen 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

dart
void main() async {
  await for (var n in Stream.fromIterable([1, 2, 3])) {
    print(n * 10);
  }
}
output
10
20
30

Example 2 — A Countdown with Stream.periodic

dart
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!');
}
output
3
2
1
Go!

Example 3 — Stream.value

dart
void main() async {
  await for (var name in Stream.value('Zayan')) {
    print('Only value: $name');
  }
  print('Stream done');
}
output
Only value: Zayan
Stream done

Example 4 — listen with All Three Handlers

dart
void main() {
  Stream.fromIterable(['Ali', 'Sara']).listen(
    (name) => print('Got $name'),
    onError: (e) => print('Error: $e'),
    onDone: () => print('Done'),
  );
}
output
Got Ali
Got Sara
Done

Example 5 — Leaving await for with break

dart
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');
}
output
Tick 0
Tick 1
Tick 2
Stopped

break leaves the loop and cancels the subscription, so even an endless stream stops.

Example 6 — Catching Stream.error with onError

dart
void main() {
  Stream<int>.error(Exception('No data')).listen(
    (value) => print('Value: $value'),
    onError: (e) => print('Handled: $e'),
    onDone: () => print('Done'),
  );
}
output
Handled: Exception: No data
Done

Example 7 — Student Marks

dart
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');
}
output
Received 90, total so far 90
Received 85, total so far 175
Received 78, total so far 253
Final total: 253

Example 8 — Shopping Total

dart
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'),
      );
}
output
Cart total: 1850.0

Example 9 — Bank Balance with a Pause

dart
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();
}
output
Checking account...
Balance: 100.0
Balance: 200.0
Balance: 300.0
All deposits in

Example 10 — Temperature Readings with a Glitch

dart
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');
  }
}
output
Temperature: 24.0°C
Stopped: Exception: Sensor offline

Common Beginner Mistakes

1. Using await instead of await for

dart
void main() async {
  var marks = Stream.fromIterable([88, 92]);
  var mark = await marks; // ✗ not a Future
  print(mark);
}
output
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

dart
void main() {
  // await for (var n in Stream.fromIterable([1, 2])) {} // ✗
}
output
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

dart
await for (var tick in Stream.periodic(Duration(seconds: 1), (i) => i)) {
  print(tick); // runs forever
}
print('Done'); // never reached

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

dart
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 exist
output
Skipped: 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: 5

Errors 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

dart
void main() {
  var ticks = Stream.periodic(Duration(milliseconds: 100), (c) => c).take(2);
  ticks.listen(print);
  ticks.listen(print); // ✗
}
output
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 Future for one result and a Stream for a series of results.
  • Prefer await for when values must be handled in order and the code after should wait for the end.
  • Use listen when the program should carry on while values arrive, or when one error should not stop the rest.
  • Always give listen an onError handler when the stream can fail; wrap await for in try/catch.
  • Give every endless stream an end: take, break, or cancel().
  • Keep the StreamSubscription if 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
dart
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');
}
output
Score: 7
Score: 9
Score: 4
Score: 10
Best score: 10

How 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

  1. Use Stream.fromIterable and await for to print 'Ali', 'Sara', and 'Zayan'.
  2. Use Stream.periodic and take to print Tick 1, Tick 2, Tick 3, one per second.
  3. Use listen with onDone to print the numbers 5 and 6, then Finished.

Application

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

    dart
    void main() {
      Stream.value('B').listen(print);
      print('A');
    }
  2. Read Stream<String>.error(Exception('Offline')) with await for and print Caught: Exception: Offline.

  3. Add up the prices 200.0, 350.0, and 50.0 from a stream with listen, and print the total in onDone.

  4. Listen to a 1-second periodic stream and cancel the subscription after about 2.5 seconds. Print Stopped after cancelling.

Challenge

  1. A periodic stream throws on index 1 and is limited with take(4). Print every value and every error, then Done, so that no readings are lost.
  2. Listen to a 300 ms periodic stream of 4 values, pause it right away, and resume it after 1 second. Print Paused and Resumed at the right moments.
  3. A stream delivers bank deposits 100.0, 250.0, 400.0, 50.0. Using await for, stop reading as soon as the balance reaches at least 700.0 and print the balance.

Practice Answers

Question 1 — Solution

dart
void main() async {
  await for (var name in Stream.fromIterable(['Ali', 'Sara', 'Zayan'])) {
    print(name);
  }
}
output
Ali
Sara
Zayan

Question 2 — Solution

dart
void main() async {
  var ticks = Stream.periodic(Duration(seconds: 1), (i) => i + 1).take(3);
  await for (var tick in ticks) {
    print('Tick $tick');
  }
}
output
Tick 1
Tick 2
Tick 3

Question 3 — Solution

dart
void main() {
  Stream.fromIterable([5, 6]).listen(print, onDone: () => print('Finished'));
}
output
5
6
Finished

Question 4 — Solution

output
A
B

listen only registers the handler; the value arrives after main's own code has run.

Question 5 — Solution

dart
void main() async {
  try {
    await for (var text in Stream<String>.error(Exception('Offline'))) {
      print(text);
    }
  } catch (e) {
    print('Caught: $e');
  }
}
output
Caught: Exception: Offline

Question 6 — Solution

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

Question 7 — Solution

dart
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');
}
output
Tick 0
Tick 1
Stopped

Question 8 — Solution

dart
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'),
  );
}
output
Reading: 20
Error: Exception: Bad reading
Reading: 22
Reading: 23
Reading: 24
Done

take(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

dart
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();
}
output
Paused
Resumed
1
2
3
4

Question 10 — Solution

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

break leaves the loop after the third deposit; the last one is never read.


Quick Revision

  1. How many values does a Future deliver? A Stream?
  2. When does the line after an await for loop run?
  3. Why does Stream.periodic usually need take?
  4. What happens to an await for loop when the stream delivers an error?
  5. What happens to listen when the stream delivers an error?
  6. What do onData, onError, and onDone each handle?
  7. What does listen return, and what can you do with it?
  8. What error appears when you listen to a single-subscription stream twice?

Key Takeaways

  • A Future delivers one value; a Stream delivers many values over time, then is done.
  • Create streams with Stream.fromIterable, Stream.periodic, Stream.value, and Stream.error.
  • Stream.periodic never ends on its own — limit it with take, which counts values only.
  • await for handles each value in order; the code after it runs when the stream is done.
  • An error ends an await for loop and is caught with try/catch around it.
  • listen takes onData, onError, and onDone, and keeps going after errors.
  • listen returns a StreamSubscription you can pause, resume, and cancel.
  • 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.


Video lecture

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