In Dart, what do `Stream.listen`'s `onData`, `onError`, `onDone` and `cancelOnError` control, and what can you do with the returned `StreamSubscription`?
answer
- three handlers and a flag
- cancelOnError defaults to false
- no onError means unhandled errors
- cancel() returns a Future
- pause and resume are counted
basics
~20 sStream.listen registers onData for each event, onError for error events, onDone for the end, and cancelOnError (default false) to stop at the first error. It returns a StreamSubscription you can pause, resume and cancel, and you must cancel it when done.
solid answer
~40 s`listen(onData, {onError, onDone, cancelOnError})` is the primitive every other stream consumer builds on. `onData` runs per data event; `onError` takes the error, or the error and a `StackTrace`; `onDone` runs when the stream closes. If you omit `onError`, error events are **unhandled** and go to the current zone's error handler. `cancelOnError` defaults to `false`, so the subscription keeps receiving events after an error unless you set it to `true`. The returned `StreamSubscription` lets you **`pause()`** (optionally with a resume-signal future), **`resume()`**, check `isPaused`, swap handlers, and **`cancel()`**, which returns a `Future<void>` that completes when the stream has finished its cleanup. Pauses are counted, so each `pause` needs a matching `resume`. Forgetting to cancel a subscription to a long-lived stream keeps its callbacks, and everything they capture, alive.
go deeper
Know the three handlers of listen, that it returns a StreamSubscription, and that you cancel it when you no longer need events.
Explain cancelOnError's false default, what happens to errors without onError, pause buffering and counting, and why cancel returns a Future.
Find leaks from never-cancelled subscriptions, avoid long pauses on broadcast streams, and await cancel when cleanup order matters.
Make subscription ownership explicit in the architecture, so every listen has an owner responsible for cancelling it at a known point in its lifecycle.
## `listen` is the primitive Every way of consuming a Dart stream, including `await for`, `first`, `toList` and `forEach`, is built on `Stream.listen`: ```dart StreamSubscription<T> listen( void onData(T event)?, { Function? onError, void onDone()?, bool? cancelOnError, }); ``` ## The handlers - **`onData`** is called once per data event. It may be `null`, in which case data events are ignored. - **`onError`** must be a `void Function(Object error)` or a `void Function(Object error, StackTrace stackTrace)`; the stream checks which shape you passed and supplies the stack trace only to the second. If `onError` is **omitted**, error events are **unhandled**: they are passed to the current zone's error handler, which by default treats them like uncaught top-level errors. - **`onDone`** is called once, when the stream sends its done event. - **`cancelOnError`** decides what an error does to the subscription. The default is **`false`**: the error is delivered and the subscription carries on receiving later events. With `true`, the subscription is cancelled automatically when the first error is delivered. Whether a stream keeps sending after an error depends on its producer: an `async*` function that throws emits the error and closes, while other sources may continue. ## The returned `StreamSubscription` `listen` returns a `StreamSubscription<T>`, the live connection between listener and stream: | Member | What it does | |---|---| | `cancel()` | stops delivery and returns a `Future<void>` that completes when the stream has cleaned up | | `pause([Future<void>? resumeSignal])` | stops delivery; events that still arrive are buffered | | `resume()` | undoes one `pause` | | `isPaused` | `true` while pauses outnumber resumes | | `onData`, `onError`, `onDone` | replace the corresponding handler | | `asFuture([value])` | turns done into a completing future and error into a failing one | ## Pausing in detail 1. While paused, **no handlers run**. Events the subscription receives are **buffered** until it resumes. 2. For a **non-broadcast** stream, the source is usually informed and can stop producing; an `async*` body, for instance, suspends at its next `yield`. 3. For a **broadcast** stream, the pause affects only that subscription, which buffers everything meanwhile. The SDK suggests cancelling and listening again instead, if intermediate events do not matter. 4. Pauses are **counted**: two `pause` calls need two `resume` calls. Passing a `resumeSignal` future resumes automatically when it completes. 5. `resume` on a subscription that is not paused is safe and does nothing. ## Cancelling, and why it matters A subscription to a **long-lived** stream, such as a temperature sensor's broadcast stream, holds a reference to your callbacks and everything they capture. If the listener's owner goes away without cancelling, the callbacks keep running and nothing is garbage-collected. The rule is simple: whoever calls `listen` keeps the subscription and cancels it when done. ```dart class TemperatureLogger { TemperatureLogger(Stream<double> temps) { _sub = temps.listen( (t) => _log.add(t), onError: (Object e, StackTrace st) => _errors.add(e), onDone: () => _log.add(double.nan), ); } final _log = <double>[]; final _errors = <Object>[]; late final StreamSubscription<double> _sub; Future<void> close() => _sub.cancel(); } ``` `cancel()` returning a future matters when cleanup is itself asynchronous: the SDK's example is a stream that must close a file before the listener can delete it, so the listener awaits `cancel()` first. ## Cancelling from inside a handler Sometimes the decision to stop is made by the data itself, such as "log readings until the temperature passes 30 degrees". The handler then needs the subscription it belongs to, which does not exist until `listen` returns. A `late` local solves it: ```dart late final StreamSubscription<double> sub; sub = temps.listen((t) { print(t); if (t > 30) sub.cancel(); }); ``` Alternatively, create the subscription with a `null` `onData` and install the handler afterwards with `sub.onData(...)`, which replaces the data handler on an existing subscription. ## Common mistakes - Omitting `onError` and then wondering why errors appear as uncaught. - Assuming the subscription ends on the first error when `cancelOnError` was left at `false`. - Pausing a broadcast subscription for a long time and building an unbounded buffer. - Never cancelling, which is the classic leak in long-lived screens and services.
- What does `StreamSubscription.asFuture()` do to your existing handlers?It overwrites the current `onDone` and `onError` handlers with ones that complete the returned future: done completes it with the given value, and an error completes it with that error and cancels the subscription, even if `cancelOnError` was false. If you omit the value and the type is non-nullable, it throws immediately.
- Why await `cancel()` rather than just calling it?Because `cancel()` returns a `Future<void>` that completes once the stream has finished its cleanup, such as closing a file or ending an `async*` body's `finally` block. If your next step depends on that cleanup, for example deleting the file, you must await the future first.
saying these in an interview costs you the question
- cancelOnError defaults to true, so the first error ends the subscription
- Errors without an onError handler are silently ignored
- A single resume() undoes any number of pause() calls
- cancel() finishes all cleanup synchronously before returning
- Subscriptions end on their own when the listener object is unused