Mastering async and await in Kotlin Coroutines

Mukesh RajputMukesh Rajput
5 min read

What Are async and await?

In Kotlin, the async function is used to launch a coroutine that performs some asynchronous task. It returns a Deferred result, which can be thought of as a future result that will be available at some point. To get this result, you use the await function, which suspends the current coroutine until the result is ready.

  • async: Starts a coroutine and returns a Deferred object, which represents a value that will be available later.

  • await: Waits for the Deferred result to be available and retrieves it.

async and await are perfect when you need to perform multiple tasks concurrently and wait for their results.

Basic Example of async and await

Let’s see a simple example where we perform two tasks concurrently using async and await.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val result1 = async {
        performTask1()
    }
    val result2 = async {
        performTask2()
    }
    // Use await() to wait for both results
    println("Result 1: ${result1.await()}")
    println("Result 2: ${result2.await()}")
}
suspend fun performTask1(): Int {
    delay(1000L)  // Simulating a long-running task
    return 10
}
suspend fun performTask2(): Int {
    delay(2000L)  // Simulating another long-running task
    return 20
}

Explanation:

  • Two tasks are performed asynchronously using async.

  • The await() function waits for the result of both tasks and prints the values once they're ready.

  • The tasks run concurrently, meaning they don’t wait for each other to complete before starting.

Benefits of async and await

  • Concurrency: Tasks run in parallel, improving performance, especially when dealing with I/O or network-bound tasks.

  • Easy Result Handling: The Deferred object allows you to access the result once it’s available.

  • Structured Concurrency: Both async and await work well within structured concurrency, meaning coroutines are properly managed and cancelled when no longer needed.

Example: Running Multiple Tasks Concurrently

Now, let’s say we have three tasks that take varying amounts of time, and we want to run them concurrently. async is the ideal tool to achieve this:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val task1 = async { performTask("Task 1", 1000L) }
    val task2 = async { performTask("Task 2", 2000L) }
    val task3 = async { performTask("Task 3", 1500L) }
    println("Waiting for tasks to finish...")

    // Waiting for all tasks to complete
    println("${task1.await()} completed")
    println("${task2.await()} completed")
    println("${task3.await()} completed")
}
suspend fun performTask(taskName: String, time: Long): String {
    delay(time)
    return taskName
}

Explanation:

  • Three tasks are launched concurrently using async.

  • We await() each task, meaning the program will wait for each task to complete and print when they are done.

  • Even though the tasks have different delays, they run concurrently, making the total runtime much shorter than if they ran sequentially.

Example: Async with Exception Handling

You can use async with try-catch blocks to handle exceptions that occur in one or more of the concurrent coroutines.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val task = async {
        try {
            riskyTask()
        } catch (e: Exception) {
            println("Caught exception: ${e.message}")
            "Error"
        }
    }
    println("Task result: ${task.await()}")
}
suspend fun riskyTask(): String {
    delay(1000L)
    throw IllegalArgumentException("Something went wrong!")
}

Explanation:

  • We use a try-catch block inside async to handle any potential exceptions.

  • The riskyTask() throws an exception, but it’s caught, and a fallback result ("Error") is returned instead of crashing the program.

When to Use async vs launch

You might wonder when to use async and launch. Here's the difference:

  • launch: Used for coroutines that don't return a result. It’s mainly used when you don’t need to wait for the coroutine to finish or return a value.

  • async: Used for coroutines that return a result. It allows you to wait for the result using await(), making it perfect for concurrent operations where results are needed.

In short:

  • Use launch if you’re not interested in the result of a coroutine.

  • Use async if you need a result from a coroutine and want to perform concurrent operations.

Example: Sequential Execution with async vs Concurrent Execution

Let’s compare how tasks are executed sequentially versus concurrently using async.

Sequential Execution (Without async):

import kotlinx.coroutines.*

fun main() = runBlocking {
    val result1 = performTask1()
    val result2 = performTask2()
    println("Result 1: $result1")
    println("Result 2: $result2")
}
suspend fun performTask1(): Int {
    delay(1000L)
    return 10
}
suspend fun performTask2(): Int {
    delay(2000L)
    return 20
}
  • In this case, performTask2() starts only after performTask1() finishes, making the total execution time 3 seconds.

Concurrent Execution (With async):

import kotlinx.coroutines.*

fun main() = runBlocking {
    val result1 = async { performTask1() }
    val result2 = async { performTask2() }
    println("Result 1: ${result1.await()}")
    println("Result 2: ${result2.await()}")
}
  • Here, both tasks are run concurrently, making the total execution time 2 seconds (the longest task).

Summary of Key Points

  • async launches a coroutine that returns a result, represented by a Deferred object.

  • await suspends the coroutine until the result is available.

  • async and await are perfect for running multiple tasks concurrently and waiting for their results.

  • They simplify concurrent programming and can handle exceptions within coroutines.

  • Use async when you need to retrieve the result of a coroutine, and use launch when you don't need a return value.

Conclusion

Using async and await in Kotlin coroutines makes writing concurrent code simple and efficient. Whether you need to run tasks in parallel, wait for results, or handle errors gracefully, async and await provide a clean solution. Feel free to experiment with the examples provided to deepen your understanding.

0
Subscribe to my newsletter

Read articles from Mukesh Rajput directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mukesh Rajput
Mukesh Rajput

Specializing in creating scalable and maintainable applications using MVVM and Clean Architecture principles. With expertise in Ktor, Retrofit, RxJava, View Binding, Data Binding, Hilt, Koin, Coroutines, Room, Realm, and Firebase, I am committed to delivering high-quality mobile solutions that provide seamless user experiences. I thrive on new challenges and am constantly seeking opportunities to innovate in the Android development space.