skip to content

In Dart, how does a sync* generator with yield and yield* work, and when does its body actually run?

level: middleimportance: nice to knowfreq 25%

answer

  1. returns an Iterable immediately
  2. body starts on first moveNext
  3. pauses at every yield
  4. each new iterator reruns the body
  5. yield* splices another iterable

basics

~20 s

A Dart sync* function returns an Iterable at once without running its body; the body runs when iterated, pausing at each yield until the next value is requested. yield* inserts every element of another iterable, and each new iteration reruns the body.

solid answer

~40 s

Marking a body `sync*` makes the function a synchronous generator: it must return `Iterable<T>` (or a supertype) and produce values with `yield`. Calling it returns the `Iterable` immediately and runs none of the body. The first `moveNext()` starts the body; each `yield` pauses it and hands out one value; the next `moveNext()` resumes after that `yield`; the end of the body or `return;` finishes the sequence. Consumers that stop early, such as `take(3)` or `first`, leave the rest of the body unexecuted, and each new iterator runs the body again from the top — nothing is cached. `yield*` delegates to another `Iterable`, which is how generators splice in lists or recurse, and the docs recommend it for recursive generators. A generator cannot `return` a value.

code

dart · 14 lines
dart
Iterable<String> errorLines(List<String> lines) sync* {
  print('scan started');
  for (final line in lines) {
    if (line.contains('ERROR')) yield line;
  }
}

void main() {
  final lines = ['ERROR a', 'INFO b', 'ERROR c', 'ERROR d'];
  final errors = errorLines(lines); // prints nothing yet

  print(errors.first); // scan started, then ERROR a
  print(errors.take(2).toList()); // scan started, then [ERROR a, ERROR c]
}

go deeper

for a junior

Recognise sync* and yield as a way to build an Iterable one value at a time.

for a middle

Explain the pause-and-resume model, the restart on each iteration, early exit with take or first, and what yield* does.

for a senior

Use generators for traversals and early-exit consumers, and avoid them where results are read repeatedly or the body has side effects.

for a principal

Weigh generator-based lazy APIs against eager List-returning APIs for readability, cost predictability and testability in shared libraries.

## What sync* does A function whose body is marked **`sync*`** is a **synchronous generator**. It must declare a return type that is `Iterable<T>` (or a supertype), and inside the body it delivers values with **`yield`** instead of returning them: ```dart Iterable<String> errorLines(List<String> lines) sync* { for (final line in lines) { if (line.contains('ERROR')) yield line; } } ``` Calling `errorLines(lines)` returns an `Iterable<String>` **immediately, without running any of the body**. The body runs only when something iterates the result. ## When the body runs The execution model, step by step: 1. Calling the function creates the `Iterable` and returns it; no statement of the body has run yet. 2. The first `moveNext()` on an iterator starts the body from the top. 3. At each `yield value`, the body **pauses**; `moveNext()` returns `true` and `current` is `value`. 4. The next `moveNext()` resumes the body right after that `yield`. 5. When the body reaches its end or a plain `return;`, `moveNext()` returns `false`. 6. Every **new iterator** — a second `for` loop, another `toList()` — starts a **fresh run of the body** from the beginning. Results are not cached. Consequences: - `errorLines(lines).take(3)` stops asking after three values, so the rest of the body never executes. - `errorLines(lines).first` runs the body only until the first `yield`. - Code before the first `yield` (say, a `print('scanning')`) runs once **per iteration**, not once per call. ## yield*: delegate to another iterable `yield* expression` inserts **every element of another `Iterable`** into the sequence, in order, as if each were yielded individually. It is how a generator splices in a list or recurses: ```dart class LogDir { LogDir(this.name, this.lines, [this.children = const []]); final String name; final List<String> lines; final List<LogDir> children; } Iterable<String> allLines(LogDir dir) sync* { yield* dir.lines; for (final child in dir.children) { yield* allLines(child); } } ``` The Dart docs recommend `yield*` for recursive generators because it improves performance compared with looping over the recursive call and yielding each element yourself. ## Rules the analyzer enforces - `yield` and `yield*` are only allowed in a `sync*` or `async*` body (`yield_in_non_generator`). - A generator cannot `return` a value, and cannot use `=>` (`return_in_generator`); use `return;` to stop early. - A `sync*` function's return type must be a supertype of `Iterable<T>` (`illegal_sync_generator_return_type`). - The yielded value must be assignable to the element type (`yield_of_invalid_type`). ## sync* compared with building a list | | `sync*` generator | function that builds a `List` | |---|---|---| | Work done at call time | none | all of it | | Early exit (`take`, `first`) | saves the remaining work | work already done | | Iterating twice | reruns the body | reads stored elements | | Memory | one element at a time | the whole list | | Side effects in the body | repeat on every iteration | happen once | A synchronous generator runs on the same isolate and thread as its caller; each `moveNext()` simply runs the body up to the next `yield`. It is not asynchronous — its counterpart for asynchronous sequences is `async*`, which returns a `Stream` and is a different topic. ## When to use it - Producing a sequence with non-trivial control flow (nested loops, recursion, conditions) that would be awkward as a `map`/`where` chain. - Sequences where consumers usually stop early. - Traversals such as walking a directory-like tree of log folders. Avoid it when the result will be iterated many times or the body has side effects — then build a `List` once, or call `toList()` on the generator's result.

  • Why does a Dart sync* function reject return 5; in its body?
    A generator's values come from `yield`; the function's result is the `Iterable` itself, created at call time. Returning a value is the compile-time error `return_in_generator`. Use `yield 5;` to emit it, and a bare `return;` to end the sequence early.
  • Is a Dart sync* generator asynchronous or run on another thread?
    No. It runs synchronously on the caller's isolate: each `moveNext()` executes the body up to the next `yield` and returns. The asynchronous counterpart is `async*`, which returns a `Stream` instead of an `Iterable`.

saying these in an interview costs you the question

  • A sync* function runs its body when it is called.
  • A sync* generator caches values for later iterations.
  • yield* yields the inner iterable as one element.
  • sync* generators run on a background thread.
  • A sync* function can return a value with return.