skip to content

What does start = CoroutineStart.LAZY change about async, and what is a subtle pitfall of lazy Deferreds in parallel decomposition?

level: seniorimportance: nice to knowfreq 30%

answer

  1. LAZY = created, not started
  2. await() starts a lazy coroutine
  3. lazy + inline await = sequential
  4. fix with start() before await
  5. default DEFAULT is eager

basics

~20 s

LAZY means the async coroutine doesn't start until you trigger it (via await() or start()). The pitfall: if you rely on await() to start lazy tasks, they run one at a time instead of in parallel.

solid answer

~40 s

Passing start = CoroutineStart.LAZY to async makes the coroutine not begin immediately; it stays created-but-not-started until something triggers it — either await() or an explicit start(). This is useful to define work up front and run it on demand, or to conditionally skip it. The subtle pitfall for parallel decomposition: with LAZY, await() is what starts the coroutine. So `val a = async(start = LAZY){..}; val b = async(start = LAZY){..}; a.await() + b.await()` runs sequentially — a.await() starts AND finishes a before b is ever started. To regain concurrency you must explicitly start() each one first (a.start(); b.start()) or call awaitAll, then await. With the default DEFAULT start, eager launch avoids this trap entirely, which is why default async is the usual choice for fan-out.

code

kotlin · 4 lines
kotlin
val a = async(start = CoroutineStart.LAZY) { slowA() }
val b = async(start = CoroutineStart.LAZY) { slowB() }
a.start(); b.start()              // start both first
val result = a.await() + b.await() // now concurrent

go deeper

for a junior

Aware that a start parameter exists and LAZY delays execution.

for a middle

Knows await() or start() triggers a lazy coroutine and that LAZY defers the body.

for a senior

Identifies the inline-await serialization pitfall and fixes it with explicit start() or eager default.

for a principal

Uses LAZY deliberately for task graphs/conditional work and reasons about trigger ordering and resource timing.

## `CoroutineStart` options `async` (and `launch`) accept a `start` parameter of type `CoroutineStart`: - `CoroutineStart.DEFAULT` — start **eagerly** (the usual case). - `CoroutineStart.LAZY` — create the coroutine but **do not start** it until triggered. - (`ATOMIC`, `UNDISPATCHED` exist for special cases.) ## What LAZY does ```kotlin val d = async(start = CoroutineStart.LAZY) { compute() } // compute() has NOT run yet d.start() // triggers it without waiting, OR val x = d.await() // triggers it AND waits ``` With `LAZY`, the body runs only after an explicit `start()` or the first `await()`. This lets you declare work in advance and decide later whether/when to run it (e.g., skip it on a cache hit). ## The parallel-decomposition pitfall Because `await()` itself triggers a lazy coroutine, awaiting lazily-started Deferreds **inline serializes them**: ```kotlin val a = async(start = CoroutineStart.LAZY) { slowA() } val b = async(start = CoroutineStart.LAZY) { slowB() } val sum = a.await() + b.await() // a starts, runs, finishes; THEN b starts ``` This runs in `tA + tB`, not `max`. With **eager** (`DEFAULT`) async, both bodies are already running before either `await()`, so you get `max`. ## Fixing lazy concurrency Start them explicitly before awaiting: ```kotlin val a = async(start = CoroutineStart.LAZY) { slowA() } val b = async(start = CoroutineStart.LAZY) { slowB() } a.start(); b.start() // both now running concurrently val sum = a.await() + b.await() ``` Or just use the default eager start, which is why default `async` is preferred for fan-out. ## When LAZY is genuinely useful - Define-now/run-later or conditional execution. - Building a graph of dependent tasks where you control trigger order. ## Key APIs/keywords - `start = CoroutineStart.LAZY` - `Deferred.start()` triggers without waiting - `await()` triggers + waits for a lazy coroutine - contrast `CoroutineStart.DEFAULT` (eager)

  • What two things can trigger a LAZY async coroutine to start?
    An explicit start() call, or the first await() on its Deferred.
  • Why is default (eager) async usually safer for parallel decomposition?
    Eager async starts both bodies before any await, so awaiting inline still overlaps; lazy needs explicit start() to avoid serialization.

saying these in an interview costs you the question

  • Thinking LAZY async runs the body immediately
  • Awaiting lazy Deferreds inline and expecting concurrency
  • Not knowing await() can start a lazy coroutine
  • Believing start() blocks/waits for completion (it only triggers)

context