skip to content

Why can't you call suspend functions like delay() inside a sequence{} block, and what mechanism enforces that?

level: seniorimportance: should knowfreq 30%

answer

  1. @RestrictsSuspension on SequenceScope
  2. Only receiver's suspend members allowed → yield/yieldAll
  3. delay()/async calls = compile error
  4. Sequence = synchronous, dispatcher-free generator
  5. Need async? use flow{} + emit

basics

~10 s

The sequence builder is built on a restricted kind of coroutine. The compiler only lets you call yield/yieldAll inside it, so you can't await real async work like delay().

solid answer

~50 s

`SequenceScope<T>` is annotated with `@RestrictsSuspension`. That annotation tells the compiler that inside a lambda with this receiver, the only suspending calls allowed are the **member** suspend functions of the receiver itself — `yield` and `yieldAll`. Calling an external `suspend` function such as `delay()` or a network call is a **compile-time error**. The rationale: a `Sequence` is a synchronous, pull-based, single-threaded abstraction. Its 'suspension' is just a cooperative pause to hand control back to the consumer; there is no dispatcher, no thread switching, no real asynchrony. Allowing arbitrary suspends would imply async behavior the abstraction can't deliver. So the sequence builder is a restricted coroutine that reuses the suspend/resume state-machine machinery purely to implement lazy generators, not concurrency. If you genuinely need async streaming, use `flow {}` instead, where `emit` plus real suspend functions are allowed.

code

kotlin · 13 lines
kotlin
// Does NOT compile:
val bad = sequence {
    yield(1)
    // delay(100) // error: Restricted suspending function
    yield(2)
}

// Use flow for real suspension:
val good = flow {
    emit(1)
    delay(100) // OK
    emit(2)
}

go deeper

for a junior

Knows you can't put async work in sequence{} and should use something else.

for a middle

Names @RestrictsSuspension and contrasts sequence vs flow at a high level.

for a senior

Explains the synchronous pull-based model and why arbitrary suspension is meaningless here.

for a principal

Connects the restricted-coroutine design to the state-machine reuse and the API contract guarantees it preserves.

## The restriction Inside `sequence { ... }` the receiver is `SequenceScope<T>`, which the standard library declares as: ```kotlin @RestrictsSuspension abstract class SequenceScope<in T> internal constructor() { abstract suspend fun yield(value: T) abstract suspend fun yieldAll(iterator: Iterator<T>) // + Iterable / Sequence overloads } ``` The `@RestrictsSuspension` annotation changes how the compiler treats suspend calls in any lambda whose receiver is so annotated: **only the suspend members of that receiver may be called**. So `yield` and `yieldAll` are fine; `delay()`, `withContext(...)`, an HTTP `suspend fun`, etc. are **rejected at compile time**. ## Why this rule exists A `Sequence` is: - **Synchronous** — values are computed on the calling thread when pulled. - **Pull-based** — the consumer drives; the producer's `yield` merely suspends the state machine to return control. - **Dispatcher-free** — there is no `CoroutineContext` with a dispatcher, no thread hop. 'Suspension' here is a clever reuse of Kotlin's coroutine **state-machine** transformation to implement a generator — pause the producer, resume it on the next `next()`. It is *not* asynchrony. If arbitrary suspends were permitted, you could write `delay(1000)` and expect the iterator to wait — but a synchronous `Iterator.next()` has nowhere to suspend to. The restriction makes the impossible un-writable. ## What to use instead For genuinely asynchronous streams use **`flow { ... }`** from kotlinx.coroutines: ```kotlin val f = flow { emit(1) delay(100) // legal: Flow is a real coroutine emit(2) } ``` `Flow`'s builder is **not** `@RestrictsSuspension`, so `emit` plus any suspend function works, and it runs in a coroutine with a real context/dispatcher. ## Mental model | | `sequence {}` | `flow {}` | |---|---|---| | Emit | `yield` | `emit` | | Suspend scope | restricted | unrestricted | | Async / delay | no | yes | | Threading | caller, synchronous | coroutine context | So: same coroutine *machinery*, opposite *contract*. The `@RestrictsSuspension` marker is exactly what enforces that contract for sequences.

  • What is the correct builder when you need delay() between emissions?
    flow {} from kotlinx.coroutines — its emit plus arbitrary suspend functions are allowed because its scope is not @RestrictsSuspension.
  • Does sequence{} use a dispatcher or run on another thread?
    No. It is synchronous and runs on the consuming thread; the coroutine machinery is reused only to implement pause/resume for lazy generation.

It's a one-lane pause button, not a phone line: it can pause the line to hand you a value, but it can't go make a real async call and come back.

saying these in an interview costs you the question

  • Thinking you can call delay() inside sequence{} at runtime
  • Confusing sequence{} with flow{} for async streaming
  • Believing sequence{} runs on a background dispatcher
  • Not knowing @RestrictsSuspension is the enforcing mechanism
  • Saying the restriction is a runtime check rather than compile-time

context