How to the async() coroutine builder in Kotlin

dimitrilc 3 Tallied Votes 325 Views Share

Introduction

launch() and async() are two of the most common coroutine builders to use in Kotlin, but they are somewhat different in usage.

launch() returns a Job object, which can be used to cancel or perform other operations on the underlying coroutine. If our coroutine lambda returned a useful result, then there is no way to access it via this Job object.

async() is different to launch() because it returns a Deferred<T> object, where T stands for the result returned from its lambda expression. Deferred<T> is actually a child of Job, and via a Deferred object, we can retrieve the underlying lambda value.

In this tutorial, we will learn how to use the async() coroutine builder.

Goals

At the end of the tutorial, you would have learned:

  1. How to use the async() coroutine builder.

Prerequisite Knowledge

  1. Intermediate Kotlin.
  2. Basic coroutine.

Tools Required

  1. A Kotlin IDE such as IntelliJ Community Edition.

Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Kotlin project, using JDK 16 as the project JDK and Gradle 7.2 as the build tool.

  2. Copy and paste the configuration code below into your build.gradle.kts file.

     import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
    
     plugins {
        kotlin("jvm") version "1.5.31"
     }
    
     group = "com.example"
     version = "1.0-SNAPSHOT"
    
     repositories {
        mavenCentral()
     }
    
     dependencies {
        implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
        testImplementation("org.jetbrains.kotlin:kotlin-test:1.5.31")
     }
    
     tasks.test {
        useJUnitPlatform()
     }
    
     tasks.withType<KotlinCompile>() {
        kotlinOptions.jvmTarget = "11"
     }
  3. Under src/main/kotlin, create a new package called com.example.

  4. Create a new Kotlin file called Entry.kt.

  5. Create the main() function inside this file.

async() Concept Overview

In order to understand how the coroutine builder async() works, let us look at its function signature:

    fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T)
    : Deferred<T>

From the method signature above, we can see that it takes 3 arguments, with the first and the second arguments being optional arguments, so we are only required to provide the last argument, which is a lambda that returns a type of T. The returned value will be wrapped inside the Deferred<T> object that the async() function returns(the return value from the block lambda expression is different from the return value of the async() function).

Compared to the launch() coroutine builder, with the async() builder, we are only mostly interested in the return value of the lambda and the Deferred<T> object. Because Deferred is a child of Job, it provides extra functionalities.

async() Usage

Because async() does not return the underlying value directly to us, we will have to get it from the Deferred object. Copy and paste the top-level function asyncVerbose() below into your Entry.kt file.

    suspend fun asyncVerbose() = coroutineScope { //1
       val deferred: Deferred<String> = async { //2
           println("waiting") //3
           delay(2000L) //4
           "result" //5
       }

       val result: String = deferred.await() //6

       println(result) //7
    }

The code above is purposely verbose to demonstrate how the underlying value is retrieved.

  1. async() must be used inside a coroutine scope, so we created a dedicated coroutine scope for it to run in at line 1.
  2. Line 2 is where we call the async(). We only have to provide the last argument, which is a lambda expression that returns some type, so the passing-trailing-lambda syntax is used here.
  3. Line 3 prints something to the console to indicate that the coroutine has started.
  4. Line 4 waits for 2 seconds, pretending that the coroutine is performing some long-running operations.
  5. Line 5 returns the result of the lambda, which is just a simple String.
  6. On line 6, we call await() on the Deferred object received from async(). await() does not block the current thread. It only pauses the coroutine until the calculation is complete. After the calculation is completed, wait() returns a result.

We can call the method in main() like this:

    fun main() = runBlocking {
       asyncVerbose()
    }

The code completes in about 2 seconds and prints:

    waiting
    result

Retrieve the result without waiting

Besides await(), it is also possible to attempt to retrieve the result immediately. For tasks that you do not want to wait for forever, you can use the function getCompleted(). This function will throw a runtime exception if the task has not been completed yet. Whether to catch this exception and log/rethrow/handle it is your choice.

Let us add another function called asyncNoWait() that will use the getCompleted() function. Copy and paste the code below into Entry.kt.

    @OptIn(ExperimentalCoroutinesApi::class) //8
    suspend fun asyncNoWait() = coroutineScope { //9
       val deferred: Deferred<String> = async {
           println("waiting")
           delay(5000L)
           "result"
       }

       delay(2000L) //10
       print("waited for too long, timing out...")

       val result: String = deferred.getCompleted() //11

       println(result)
    }

In the code snippet above, the async lambda is actually waiting for 5 seconds. At line 10, the application is only waiting for 2 seconds before giving up and moving on to call getCompleted(). Because the async() lambda has not completed yet, the code throws an IllegalStateException:

    waiting
    waited for too long, timing out...Exception in thread "main" java.lang.IllegalStateException: This job has not completed yet
        at kotlinx.coroutines.JobSupport.getCompletedInternal$kotlinx_coroutines_core(JobSupport.kt:1199)
        at kotlinx.coroutines.DeferredCoroutine.getCompleted(Builders.common.kt:100)
        at com.example.EntryKt$asyncNoWait$2.invokeSuspend(Entry.kt:33)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:234)
        at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
        at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:397)
        at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:431)
        at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:420)
        at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:518)
        at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:489)
        at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
        at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
        at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
        at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
        at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
        at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
        at com.example.EntryKt.main(Entry.kt:5)
        at com.example.EntryKt.main(Entry.kt)

Solution Code

    package com.example

    import kotlinx.coroutines.*

    fun main() = runBlocking {
       //asyncVerbose()
       asyncNoWait()
    }

    suspend fun asyncVerbose() = coroutineScope { //1
       val deferred: Deferred<String> = async { //2
           println("waiting") //3
           delay(2000L) //4
           "result" //5
       }

       val result: String = deferred.await() //6

       println(result) //7
    }

    @OptIn(ExperimentalCoroutinesApi::class) //8
    suspend fun asyncNoWait() = coroutineScope { //9
       val deferred: Deferred<String> = async {
           println("waiting")
           delay(5000L)
           "result"
       }

       delay(2000L) //10
       print("waited for too long, timing out...")

       val result: String = deferred.getCompleted() //11

       println(result)
    }

Summary

We have learned how to use the async() coroutine builder. It should be used whenever we expect a value from the lambda expression.

The full project code can be found here https://github.com/dmitrilc/DaniwebCoroutineAsyncKotlin/tree/master