skip to content

What does the start parameter of launch control, and how does CoroutineStart.LAZY differ from CoroutineStart.DEFAULT?

level: middleimportance: should knowfreq 55%

answer

  1. start = WHEN the coroutine runs
  2. DEFAULT = dispatched immediately
  3. LAZY = waits for start() or join()
  4. ATOMIC = uncancellable until first suspend
  5. UNDISPATCHED = run now on current thread until first suspend

basics

~10 s

The start parameter decides when the coroutine actually begins running. DEFAULT starts it right away; LAZY creates the Job but waits to run until you trigger it with start() or join().

solid answer

~50 s

launch's second parameter is start: CoroutineStart, defaulting to CoroutineStart.DEFAULT. With DEFAULT, the coroutine is scheduled to run immediately (dispatched per its dispatcher). With CoroutineStart.LAZY, launch returns a Job in the 'New' state without executing the block; it only begins when you call job.start() or job.join() (or otherwise await it). LAZY is useful to build a Job ahead of time and trigger it later, or to set up a dependency graph. Other CoroutineStart values exist: ATOMIC (starts and is non-cancellable until the first suspension point — cannot be cancelled before it runs) and UNDISPATCHED (runs immediately in the current thread up to the first suspension, skipping the dispatcher). A common gotcha: a LAZY job that you never start and never join will simply never run, yet a structured parent will still wait for it as a child — so an unstarted lazy child can hang the scope unless started.

code

kotlin · 7 lines
kotlin
fun main() = runBlocking {
    val lazy = launch(start = CoroutineStart.LAZY) { println("B: lazy ran") }
    println("A: created")
    lazy.start()        // triggers it; without this it never runs
    lazy.join()
    println("C: done")
}

go deeper

for a junior

Knows DEFAULT runs now and LAZY waits, can trigger LAZY with start().

for a middle

Explains start()/join() triggering, and the structured-concurrency hang gotcha for unstarted lazy children.

for a senior

Distinguishes ATOMIC and UNDISPATCHED semantics and when each matters (guaranteed cleanup, low-latency start).

for a principal

Weighs CoroutineStart choices against cancellation guarantees and dispatcher behavior in production code, and avoids subtle hangs/leaks from lazy children.

## The `start` parameter `launch` takes a `start: CoroutineStart` parameter: ```kotlin launch(start = CoroutineStart.LAZY) { /* ... */ } ``` `CoroutineStart` is an enum controlling **when and how** the coroutine begins executing. The default is `CoroutineStart.DEFAULT`. ## `DEFAULT` The coroutine is **immediately scheduled** for execution according to its dispatcher. 'Immediately scheduled' (not necessarily 'immediately running') — it's dispatched, so on `Dispatchers.Default`/`IO` it runs on a worker thread as soon as one is free. If it is cancelled before it starts, it may never run its body. ## `LAZY` The coroutine is **created but not started**. `launch` returns a `Job` in the *New* state; the block does not execute yet. It begins only when you explicitly trigger it: - `job.start()` — starts it and returns `true` if it was actually started. - `job.join()` — starts it (if not started) and then suspends until completion. ```kotlin fun main() = runBlocking { val job = launch(start = CoroutineStart.LAZY) { println("running now") } println("created but not running yet") job.start() // -> prints "running now" } ``` Use `LAZY` when you want to **prepare** a coroutine and decide later whether/when to run it, or to wire up ordered dependencies. ### LAZY gotcha with structured concurrency A lazy child you never start still belongs to the parent. A structured parent (e.g. `coroutineScope`) **waits for all children**, including an unstarted lazy one — but an unstarted lazy job never completes on its own, so the scope can **hang**. Always start lazy jobs (or don't make them children you forget). ## Other `CoroutineStart` values - **`ATOMIC`** — schedules like DEFAULT but the coroutine is **non-cancellable until it reaches its first suspension point**. So even if cancelled early, its body up to the first suspend runs. Useful to guarantee setup/cleanup code executes. - **`UNDISPATCHED`** — starts running **immediately in the current thread** until the first suspension point, *skipping* the dispatcher for that initial slice; after the first suspend it resumes on the dispatcher. Useful for low-latency immediate execution. ## Summary table - `DEFAULT`: dispatched immediately; cancellable before start. - `LAZY`: not started until `start()`/`join()`. - `ATOMIC`: dispatched immediately; not cancellable until first suspend. - `UNDISPATCHED`: runs now on current thread up to first suspend.

  • What happens to a LAZY child that you never start, inside a coroutineScope?
    It never executes on its own, but the parent still treats it as a child and waits for it, so the scope can hang. You must call start() (or join()).
  • Which two calls will start a LAZY job?
    job.start() and job.join() both start a lazy job; join() additionally suspends until it completes.

saying these in an interview costs you the question

  • Saying LAZY runs the block but defers only the result
  • Not knowing start() or join() triggers a LAZY job
  • Claiming DEFAULT guarantees the block runs even if cancelled first
  • Confusing UNDISPATCHED with LAZY

context