Composing Suspending Functions with Kotlin Coroutines

Sequential Invocation

Consider defining two distinct suspending functions that perform operations like remote service calls or computations. The functions themselves are presumed to be useful, but for this example, they simply delay for one second before returning a result:

suspend fun fetchFirstValue(): Int {
    delay(1000L) // Simulates a useful task
    return 13
}

suspend fun fetchSecondValue(): Int {
    delay(1000L) // Simulates another useful task
    return 29
}

When calls must be made sequentially—perhaps because the second call depends on the outcome of the first—a standard procedural order is used. Since this code runs inside a coroutine, sequential execution is the default. The following snippet measures the total time taken to execute both suspending functions:

val duration = measureTimeMillis {
    val resultOne = fetchFirstValue()
    val resultTwo = fetchSecondValue()
    println("Result sum: ${resultOne + resultTwo}")
}
println("Total execution time: $duration ms")

Output:

Result sum: 42
Total execution time: 2017 ms

Concurrent Execution Using async

If fetchFirstValue and fetchSecondValue are independent and you wish to obtain results faster, they can be executed concurrently with async. Conceptually, async is similar to launch. It initiates a separate lightweight coroutine that works concurrently. The key distinction is that launch returns a Job without a result, while async returns a Deferred<T>—a non-blocking future that promises a result later. You can retrieve the final result using .await() on the deferred value. Deferred also extends Job, allowing cancellation if needed.

val duration = measureTimeMillis {
    val deferredOne = async { fetchFirstValue() }
    val deferredTwo = async { fetchSecondValue() }
    println("Result sum: ${deferredOne.await() + deferredTwo.await()}")
}
println("Total execution time: $duration ms")

Output:

Result sum: 42
Total execution time: 1017 ms

The execution time is roughly halved because the two coroutines run in parallel. Concurrency with coroutines is always explicit.

Lazily Started Async

Optionally, async can be made lazy by setting its start parameter to CoroutineStart.LAZY. In this mode, the coroutine only begins execution when its result is requested via await or when its start function is explicitly invoked.

val duration = measureTimeMillis {
    val deferredOne = async(start = CoroutineStart.LAZY) { fetchFirstValue() }
    val deferredTwo = async(start = CoroutineStart.LAZY) { fetchSecondValue() }
    // Perform some other computation here
    deferredOne.start() // Initiate the first coroutine
    deferredTwo.start() // Initiate the second coroutine
    println("Result sum: ${deferredOne.await() + deferredTwo.await()}")
}
println("Total execution time: $duration ms")

Output:

Result sum: 42
Total execution time: 1017 ms

The two coroutines defined earlier do not execute immediately; the programmer controls their initiation via start. Calling start on each before await allows them to run concurrently. If await is called directly without a prior start, execution becomes sequentila, as await starts the coroutine and waits for its completion—this is not the intended use case for laziness. The pattern async(start = CoroutineStart.LAZY) can serve as an alternative to the standard library's lazy function when the computation involves suspending functions.

Functions with Async Style

You can define functions in an asynchronous style that call fetchFirstValue and fetchSecondValue using the async builder with an explicit GlobalScope reference. Such functions are often named with an "Async" suffix to indicate they perform asynchronous computation and return a Deferred result.

// Returns Deferred<Int>
fun fetchFirstValueAsync() = GlobalScope.async {
    fetchFirstValue()
}

// Returns Deferred<Int>
fun fetchSecondValueAsync() = GlobalScope.async {
    fetchSecondValue()
}

Note that these *Async functions are not suspending functions. They can be called from anywhere. However, their invocation implies asynchronous (concurrent) execution relative to the caller.

The example below demonstrates their usage outside a coroutine scope:

// Note: `runBlocking` is not wrapped around the main function here.
fun main() {
    val duration = measureTimeMillis {
        // Launch async execution outside a coroutine
        val deferredOne = fetchFirstValueAsync()
        val deferredTwo = fetchSecondValueAsync()
        // To wait for results, a suspending or blocking context is required.
        // Here, `runBlocking` blocks the main thread.
        runBlocking {
            println("Result sum: ${deferredOne.await() + deferredTwo.await()}")
        }
    }
    println("Total execution time: $duration ms")
}

This asynchronous programming style is presented for reference, as it is common in other languages. Its use with Kotlin coroutines is strongly discouraged for the reasons outlined below.

Consider a scenario where a logical error occurs between the line val deferredOne = fetchFirstValueAsync() and the deferredOne.await() expression, causing an exception and aborting the program. Typicaly, a global exception handler might log the error, but the program could continue other operations. Meanwhile, fetchFirstValueAsync would still be running in the background, even though the operation that started it has terminated. This approach lacks structured concurrency, as explained in the next section.

Structured Concurrency with Async

Take the concurrent example using async and extract a function that concurrently calls fetchFirstValue and fetchSecondValue, returning the sum of their results. Since async is an extension on CoroutineScope, it must be called within a scope, which the coroutineScope builder provides.

suspend fun computeConcurrentSum(): Int = coroutineScope {
    val deferredOne = async { fetchFirstValue() }
    val deferredTwo = async { fetchSecondValue() }
    deferredOne.await() + deferredTwo.await()
}

With this structure, if an error occurs inside computeConcurrentSum and an exception is thrown, all coroutines launched within its scope are automatically cancelled.

val duration = measureTimeMillis {
    println("Result sum: ${computeConcurrentSum()}")
}
println("Total execution time: $duration ms")

Output from the main function shows both operations still execute concurrently:

Result sum: 42
Total execution time: 1017 ms

Cancellation is always propagated through the coroutine hierarchy:

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    try {
        failedConcurrentSum()
    } catch (e: ArithmeticException) {
        println("Computation failed with ArithmeticException")
    }
}

suspend fun failedConcurrentSum(): Int = coroutineScope {
    val deferredOne = async<Int> {
        try {
            delay(Long.MAX_VALUE) // Simulates a long-running computation
            42
        } finally {
            println("First child coroutine was cancelled")
        }
    }
    val deferredTwo = async<Int> {
        println("Second child coroutine throws an exception")
        throw ArithmeticException()
    }
    deferredOne.await() + deferredTwo.await()
}

If one child coroutine (e.g., deferredTwo) fails, the other async operasion (deferredOne) and the awaiting parent coroutine are both cancelled:

Second child coroutine throws an exception
First child coroutine was cancelled
Computation failed with ArithmeticException

Tags: kotlin Coroutines Asynchronous Programming Concurrency Structured Concurrency

Posted on Thu, 23 Jul 2026 16:18:24 +0000 by CFennell