LAZY

Starts the coroutine lazily, only when it is needed.

Starting a coroutine with LAZY only creates the coroutine, but does not schedule it for execution. When the completion of the coroutine is first awaited (for example, via Job.join) or explicitly started, the dispatch procedure described in the CoroutineDispatcher documentation is performed in the thread that did it.

The details of what counts as waiting can be found in the documentation of the corresponding coroutine builders like launch and async.

If the coroutine's Job is cancelled before it started executing, then it will not start its execution at all and will be considered cancelled.

Pitfall: launching a coroutine with LAZY without awaiting or cancelling it at any point means that it will never be completed, leading to deadlocks and resource leaks. For example, the following code will deadlock, since coroutineScope waits for all of its child coroutines to complete:

// This code hangs!
coroutineScope {
launch(start = CoroutineStart.LAZY) { }
}

The behavior of LAZY can be described with the following examples:

// Example of lazily starting a new coroutine that goes through a dispatch
runBlocking {
println("1. About to start a new coroutine.")
// Create a job to execute on `Dispatchers.Default` later.
val job = launch(Dispatchers.Default, start = CoroutineStart.LAZY) {
println("3. Only now does the coroutine start.")
}
delay(10.milliseconds) // try to give the coroutine some time to run
println("2. The coroutine still has not started. Now, we join it.")
job.join()
}
// Example of lazily starting a new coroutine that doesn't go through a dispatch initially
runBlocking {
println("1. About to lazily start a new coroutine.")
// Create a job to execute on `Dispatchers.Unconfined` later.
val lazyJob = launch(Dispatchers.Unconfined, start = CoroutineStart.LAZY) {
println("3. The coroutine starts on the thread that called `join`.")
}
// We start the job on another thread for illustrative purposes
launch(Dispatchers.Default) {
println("2. We start the lazyJob.")
job.start() // runs lazyJob's code in-place
println("4. Only now does the `start` call return.")
}
}

Alternatives

The effects of LAZY can usually be achieved more idiomatically without it.

When a coroutine is started with LAZY and is stored in a property, it may be a better choice to use lazy instead:

// instead of `val page = scope.async(start = CoroutineStart.LAZY) { getPage() }`, do
val page by lazy { scope.async { getPage() } }

This way, the child coroutine is not created at all unless it is needed. Note that with this, any access to this variable will start the coroutine, even something like page.invokeOnCompletion { } or page.isActive.

If a coroutine is started with LAZY and then unconditionally started, it is more idiomatic to create the coroutine in the exact place where it is started:

// instead of `val job = scope.launch(start = CoroutineStart.LAZY) { }; job.start()`, do
scope.launch { }