skip to content

Asynchrony

Dart's asynchronous constructs: futures with async and await, the event and microtask queues, single and broadcast streams, stream controllers and zones. Interviewers use them to test the event loop.

part ofDartoverview, primer and where to startread it →
on this pageshow

explore

questions

26

In Dart, what does a StreamController give you, and whose job is it to call close() on it?

level: juniorimportance: must knowfreq 62%

answer

  1. two ends of one pipe
  2. add, addError, addStream on the input
  3. hand out stream, keep the controller
  4. creator closes, listeners cancel
  5. close() sends done, then add throws

basics

~10 s

A StreamController pairs an input side (add, addError, addStream, close) with the stream it feeds. The code that creates the controller owns it and must close it; listeners only cancel their own subscriptions.

solid answer

~40 s

`StreamController<T>` from `dart:async` gives you a `stream` to hand to listeners and a sink side — `add`, `addError`, `addStream` and `close` (also exposed as the narrower `sink` view). By default it is single-subscription and delivers events asynchronously. The owner — the class or `State` that created it — keeps the controller private, exposes only `controller.stream`, and calls `close()` in its teardown. `close()` queues a done event; after it, `add` throws a `StateError`. Listeners never close the controller: they `cancel()` their subscription. One trap: the `Future` returned by `close()` never completes if a single-subscription controller was never listened to, so do not blindly `await` it in `dispose`.

code

dart · 17 lines
dart
import 'dart:async';

class TypingIndicator {
  final _controller = StreamController<bool>();

  Stream<bool> get changes => _controller.stream;

  void setTyping(bool typing) {
    if (_controller.isClosed) return;
    _controller.add(typing);
  }

  void dispose() {
    // The owner closes. Not awaited: done never completes if nobody listened.
    unawaited(_controller.close());
  }
}

go deeper

for a junior

Recall the two ends: stream for listeners, add/addError/close for the producer. Say clearly that the creator closes the controller and listeners only cancel.

for a middle

Explain what close() does: queues done after buffered events, makes add throw StateError, is idempotent, and returns the done future.

for a senior

Point out the hang: done never completes for an unlistened single-subscription controller, so awaiting close() in dispose can stall teardown. Mention opting in to the close_sinks lint.

for a principal

Frame controller ownership as an API rule for a codebase: private controllers, public streams, one owner with a dispose contract, reviewed the same way as any other resource handle.

## Two ends of one pipe A **`StreamController<T>`** (library `dart:async`) is the standard way to build a `Stream<T>` whose events come from ordinary imperative code — a button handler, a socket callback, a timer — rather than from an `async*` function. It gives you two ends: - the **output end**, `controller.stream`, which you hand to whoever wants to listen; - the **input end**, the controller itself, which implements `StreamSink<T>`: `add(event)`, `addError(error, [stackTrace])`, `addStream(source)` and `close()`. The controller also offers `controller.sink`, a view that exposes *only* the `StreamSink` interface. Passing `sink` rather than the whole controller lets a producer push events without being able to swap the lifecycle callbacks or inspect `hasListener`. The default constructor creates a **single-subscription** controller (one listener, ever) that is **asynchronous** (`sync: false`): each event is delivered to the listener in a later microtask, not inside the `add` call. ## Who owns it The rule interviewers look for is simple: **whoever creates the controller closes it.** In practice: 1. Keep the controller in a private field (`final _controller = StreamController<Message>();`). 2. Expose only `Stream<Message> get messages => _controller.stream;`. 3. Call `_controller.close()` in the owner's teardown — a service's `dispose()`, a Flutter `State.dispose()`, or the `close()` of whatever object wraps it. Listeners are on the other side of the pipe. They end their interest by calling `cancel()` on their `StreamSubscription`; they never call `close()` on someone else's controller. Mixing the two up is how apps end up with half-dead streams that a second screen can no longer use. ## What close() does | Call | Effect | |---|---| | `close()` the first time | Marks the controller closed and queues a **done** event after any buffered events | | `close()` again | Allowed; returns the same `done` future and has no further effect | | `add` / `addError` after close | Throws `StateError('Cannot add event after closing')` | | `isClosed` | `true` as soon as `close()` has been called, even if done is not yet delivered | `close()` returns the same future as `controller.done`. It completes when the done event has been delivered and the listener has stopped listening. For a single-subscription controller that **never got a listener**, or whose listener paused and never resumed, the done event is never sent and **that future never completes**. So `await _controller.close()` inside a `dispose` can hang forever; fire-and-forget it with `unawaited(...)` unless you really need to know delivery finished. ## Why forgetting to close hurts - Listeners never receive `onDone`, so an `await for` loop over the stream never exits and code after it never runs. - Whatever feeds the controller — a periodic `Timer`, a socket subscription — keeps running and keeps the controller and its buffered events reachable. - On a single-subscription controller with no listener, every `add` is **buffered**, so a forgotten producer grows memory without bound. The Dart linter has a `close_sinks` rule that flags sinks you create but never close. It is stable but belongs to **no** preset set, so you opt in to it in `analysis_options.yaml`. ## A chat-app example In a chat client, a typing-indicator service might own a `StreamController<bool>`, call `add(true)` and `add(false)` as the user types, expose `Stream<bool> get changes`, and close the controller when the conversation screen is torn down. The widget that listens (for example through a `StreamBuilder`) only subscribes and lets its subscription be cancelled; it never closes the service's controller.

  • Why can await controller.close() hang in a dispose method?
    `close()` returns the controller's `done` future, which completes only after the done event reaches the listener. If a single-subscription controller was never listened to, or its listener paused and never resumed, done is never delivered, so the future never completes. Use `unawaited(controller.close())` unless you truly need delivery confirmation.
  • What is the difference between handing out controller.sink and the controller itself?
    `controller.sink` is a `StreamSink<T>` view: `add`, `addError`, `addStream`, `close` and `done`. It hides `stream`, `onListen`, `onPause`, `onResume`, `onCancel`, `hasListener` and `isPaused`, so a producer cannot re-wire lifecycle callbacks or listen to its own output.

saying these in an interview costs you the question

  • Listeners should call close() on the controller when they are done.
  • An unclosed StreamController is harmless because the garbage collector closes it.
  • Calling add after close() is silently ignored.
  • Calling close() twice throws an error.
  • await controller.close() always completes once close is called.
open as a page

In Dart, what does a function marked `async` return to its caller, and what does `await` do inside it?

level: juniorimportance: must knowfreq 82%

basics

~20 s

An async Dart function always returns a Future<T> immediately; returning a value completes it and throwing completes it with an error. Inside, await suspends the function until a future completes, then resumes with its value or rethrows its error.

open as a page

In Dart, how does a StreamController.broadcast() controller behave differently from a default StreamController when events are added?

level: middleimportance: must knowfreq 52%

basics

~20 s

A default controller allows one listener and buffers events added before it subscribes or while it is paused. A broadcast controller allows many listeners, drops events added while nobody listens, and lets each subscription buffer its own pauses.

open as a page

In Dart, which APIs put work on an isolate's microtask queue versus its event queue, and which queue does the event loop drain first?

level: middleimportance: must knowfreq 55%

basics

~10 s

Each isolate's event loop empties the microtask queue before taking the next event. scheduleMicrotask, Future.microtask and callbacks on completed futures are microtasks; Future(), Timer.run, Future.delayed, I/O and port messages are events.

open as a page

In Dart, a dashboard loads profile, feed and stats from three independent futures; how do you run them concurrently and collect typed results?

level: middleimportance: must knowfreq 68%

basics

~20 s

Start all three futures before awaiting any: Future.wait([a, b, c]) gives a List in argument order, and Dart 3's (a, b, c).wait gives a typed record. Awaiting one after another serialises the calls and sums their latencies.

open as a page

In Dart, what do `Stream.listen`'s `onData`, `onError`, `onDone` and `cancelOnError` control, and what can you do with the returned `StreamSubscription`?

level: middleimportance: must knowfreq 58%

basics

~20 s

Stream.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.

open as a page

In Dart, why does a second `listen` on a single-subscription stream throw, and how does a broadcast stream behave differently?

level: middleimportance: must knowfreq 70%

basics

~20 s

A single-subscription Dart stream allows exactly one listener for its whole lifetime, so a second listen throws a StateError, even after the first cancelled. A broadcast stream accepts any number of listeners, but each only sees events emitted after it subscribed.

open as a page

In a Dart server job, an unawaited Future fails after its surrounding try/catch has already finished — where does that error go?

level: seniorimportance: must knowfreq 45%

basics

~20 s

A failed Future with no listener is an uncaught asynchronous error: it goes to the handleUncaughtError of the zone the future belongs to. In the root zone of a standalone Dart program that kills the process; inside runZonedGuarded it reaches onError instead.

open as a page

In Dart, when main() calls an async function without awaiting it, which of that function's statements run before main's next line?

level: juniorimportance: should knowfreq 46%

basics

~20 s

Every statement before the first await runs immediately, inside the call. At the first await the function suspends, returns an uncompleted Future, and main continues; the rest of the body resumes later from the event loop.

open as a page

In Dart, how does an `await for` loop consume a stream, and what happens on an error, a `break`, or an endless stream?

level: juniorimportance: should knowfreq 55%

basics

~20 s

await for, allowed only in async functions, runs its body per stream event until the stream closes. An error event is rethrown at the loop, break or return unsubscribes, and an endless stream keeps the function waiting forever.

open as a page

In Dart, how do Timer and Timer.periodic differ from Future.delayed, and how do cancel() and isActive let you stop one?

level: juniorimportance: should knowfreq 46%

basics

~20 s

Timer runs a callback once after a delay and Timer.periodic repeats it, and both return a handle with cancel() and isActive. Future.delayed also waits but cannot be cancelled, so use a Timer whenever the work might need stopping.

open as a page

In Dart, how do Stream.asyncMap and Stream.asyncExpand differ from map, and do they process events concurrently?

level: middleimportance: should knowfreq 33%

basics

~20 s

asyncMap lets the per-event function return a Future and waits for it; asyncExpand maps each event to a whole stream and emits all its events. Both pause the source while that work runs, so events are processed one at a time, in order.

open as a page

In Dart, how would you use a StreamTransformer and transform() to turn a socket's raw bytes into a stream of chat messages?

level: middleimportance: should knowfreq 38%

basics

~10 s

Chain transformers: decode the socket's bytes to text, split into lines, then parse each line with a StreamTransformer built by StreamTransformer.fromHandlers, whose handleData adds a ChatMessage to its sink. transform(t) simply calls t.bind(stream).

open as a page

In Dart, when do a StreamController's onListen, onPause, onResume and onCancel callbacks fire, and what should each one do?

level: middleimportance: should knowfreq 42%

basics

~20 s

onListen fires when the stream gets its listener and should start the source; onPause and onResume fire as the subscription pauses and resumes and should stop and restart it; onCancel fires when the subscription ends and should release it.

open as a page

In Dart, how do you predict the print order when main() mixes synchronous prints, scheduleMicrotask, Future.microtask, Future(), Timer.run and then callbacks?

level: middleimportance: should knowfreq 48%

basics

~20 s

Run synchronous code first, then every queued microtask in scheduling order, then one event at a time with a full microtask drain after each. Future() and Timer.run are events; a then on a pending future runs the moment it completes.

open as a page

In Dart, how do `then`, `catchError` and `whenComplete` chains map onto `await` with try/catch/finally, and which should you prefer?

level: middleimportance: should knowfreq 48%

basics

~10 s

then runs code with the value, catchError handles errors like catch, and whenComplete runs cleanup like finally. The same flow written with await inside try/catch/finally is clearer, and Effective Dart prefers it.

open as a page

In Dart, how does an `async*` function produce a stream with `yield` and `yield*`, and what happens to its body on pause and cancel?

level: middleimportance: should knowfreq 45%

basics

~20 s

An async* function returns a single-subscription Stream immediately and runs its body only once listened to; yield emits one event and yield* forwards another stream's events. It pauses at yield while paused, and after cancel the next yield returns, running finally blocks.

open as a page

In Dart, how do you turn a callback-based API into a Future with a Completer, and what goes wrong if you complete it incorrectly?

level: middleimportance: should knowfreq 40%

basics

~10 s

Create a Completer<T>, return completer.future, and call complete(value) or completeError(error, stackTrace) exactly once from the callbacks. Completing twice throws StateError; never completing leaves every awaiter hanging.

open as a page

A Dart StreamController republishing chat messages from a socket keeps growing in memory while its slow listener is paused — why, and how do you fix it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

A StreamController never refuses add: while its listener is paused, or before it listens, every event is queued. If the socket callback keeps calling add, the queue grows. Pause the socket subscription in onPause, or pipe it with addStream.

open as a page

In Dart, what do `Future.timeout` and `Future.any` actually do to the futures they give up on, and why doesn't either stop the slow work?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Future.timeout returns a new future that fails with TimeoutException, or uses onTimeout, if the source is late; Future.any completes with the first result, value or error. Neither cancels anything: the losing work keeps running and its result is discarded.

open as a page

In Dart, what goes wrong when a future is neither awaited nor handled, and when should you reach for `unawaited()` or `ignore()` instead?

level: seniorimportance: should knowfreq 38%

basics

~20 s

An unawaited future lets the next statement run before its work finishes, and its error becomes unhandled. Use unawaited(f) from dart:async to mark deliberate fire-and-forget while still surfacing errors, and f.ignore() only when even its errors do not matter.

open as a page

In Dart, what does `asBroadcastStream()` do with the underlying subscription as listeners come and go, and when do its `onListen` and `onCancel` callbacks matter?

level: seniorimportance: should knowfreq 28%

basics

~20 s

asBroadcastStream subscribes to the source when its first listener arrives and stays subscribed until the source ends or a callback cancels it, even with no listeners, so events fired meanwhile are lost. onListen and onCancel let you pause, resume or cancel the source.

open as a page

In Dart, what does runZonedGuarded do, and why can a Future that fails inside it never complete when awaited from outside?

level: seniorimportance: should knowfreq 30%

basics

~20 s

runZonedGuarded runs its body in a new error zone whose onError receives uncaught asynchronous errors and synchronous throws. Future errors never cross error-zone boundaries, so a future failing inside it goes to onError and never completes for an outside awaiter.

open as a page

In Dart, what is `FutureOr<T>`, and why does Effective Dart accept it in parameters and callbacks but not as a return type?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

FutureOr<T> is Dart's union of T and Future<T>: a value that is either a plain T or a future of one. Accept it in parameters and callback return types, but return Future<T>, so callers never check which they got.

open as a page

A Flutter screen freezes although no single Dart callback is slow; how can the microtask queue starve the event queue, and how do you fix it?

level: seniorimportance: nice to knowfreq 24%

basics

~20 s

The event loop drains every microtask, including newly added ones, before taking any event, so an unbroken microtask chain blocks timers, input and frames. Break it by yielding through a zero-duration timer every N items, or move heavy work to an isolate.

open as a page

In Dart, what can a zone carry through zoneValues and intercept through a ZoneSpecification, and when would a server job use them?

level: seniorimportance: nice to knowfreq 16%

basics

~20 s

zoneValues attach read-only context, such as a job id, that every callback in the zone sees via Zone.current[key]. A ZoneSpecification overrides zone operations such as print, timer and microtask creation, callback registration and uncaught-error handling.

open as a page