Kotlin Coroutines By Example (Exception Handling, Delay, Timeout & More)

Kotlin Coroutines
Get introduced to coroutines, their implementation in Kotlin using the kotlinx.coroutines library, and learn how to use them to launch asynchronous and non-blocking operations on the JVM and on Android in particular.

Introduction

What are coroutines

Coroutines are a simple way to implement asynchronous and non-blocking code. They rely on functions that have the ability to suspend their execution at some point and resume later on: suspending functions. They cooperatively take turn to execute, thus enabling a non-blocking style of programming.

Compared to threads, they are:

  • Light-weight and don't require a lot of system resources: one thread can run many coroutines.
  • Safer and less error-prone.

Coroutines in Kotlin

Kotlin only provides support to suspending functions at the language level, and relies on libraries to implement high-level coroutine-enabled primitives.

kotlinx.coroutines is the primary coroutine library for Kotlin.

A coroutine can be launched by invoking a coroutine builder inside a coroutine scope (context).

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch { // launch a new coroutine
        delay(500L)
        println("World!")
    }
    println("Hello")
    runBlocking {
        delay(1000L)
    }
}
  • A coroutine scope is an entity responsible for controlling the lifecycle of a group of coroutines and their context. Scopes wait for all their associated coroutines to complete before completing themselves.
  • Coroutine builders are extension functions of coroutine scopes. They launch new coroutine and return either a Job (ie: launch) or a Deferred value that will hold a future result (ie: async).

Suspending functions are marked with the suspend modifier in Kotlin. They can be used inside coroutines just like regular functions, but their additional feature is that they can, in turn, use other suspending functions such as those provided by the standard library (ie: delay).

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch { printWorld() }
    println("Hello")
}

suspend fun printWorld() {
    delay(1000L)
    println("World!")
}

Coroutine scopes

  • GlobalScope is a coroutine scope used to launch top-level coroutines which are operating on the whole application lifetime and are not cancelled prematurely.
  • A new coroutine scope can be defined using CoroutineScope factory functions such as CoroutineScope(context) and MainScope.
  • To create a coroutine scope and launch a given suspending block of code within this scope, scope builders such as coroutineScope, withContext or runBlocking can be used (the latter blocks the current thread while waiting for it's completion)
  • To specify the kind of thread where the coroutine should be executed, a CoroutineDispatcher can be specified as an argument when creating a coroutine scope such as Dispatchers.IO (and Dispatchers.Main to execute on the main UI thread of the platform as explained in the following sections).
  • The plus + operator can be used to merge multiple coroutine contexts

GlobalScope launch
Simple coroutine scope with GlobalScope
coroutineScope + withContext
coroutineScope and withContext usage examples
CoroutineScope with plus operator
Coroutine context merge with + operator

Execute code in the background and continue

import kotlinx.coroutines.*

fun main() {
    println("The main program is started")
    GlobalScope.launch {
        println("Background processing started")
        delay(500L)
        println("Background processing finished")
    }
    println("The main program continues")
    runBlocking {
        delay(1000L)
        println("The main program is finished")
    }
}

Execute code in the background and continue

Concurrent code execution

Launch multiple computations in parallel and wait for them to finish

import kotlinx.coroutines.*
import java.text.SimpleDateFormat
import java.util.*

fun main() = runBlocking {
    val deferred1 = async { computation1() }
    val deferred2 = async { computation2() }
    printCurrentTime("Awaiting computations...")
    val result = deferred1.await() + deferred2.await()
    printCurrentTime("The result is $result")
}

suspend fun computation1(): Int {
    delay(1000L) // simulated computation
    printCurrentTime("Computation1 finished")
    return 131
}

suspend fun computation2(): Int {
    delay(2000L)
    printCurrentTime("Computation2 finished")
    return 9
}

fun printCurrentTime(message: String) {
    val time = (SimpleDateFormat("hh:mm:ss")).format(Date())
    println("[$time] $message")
}

Concurrent code execution

Cancelling a coroutine execution

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        // Emulate some batch processing
        repeat(30) { i ->
            println("Processing $i ...")
            delay(300L)
        }
    }
    delay(1000L)
    println("main: The user requests the cancellation of the processing")
    job.cancelAndJoin() // cancel the job and wait for it's completion
    println("main: The batch processing is cancelled")
}

Cancelling a coroutine execution

Execution timeout

Run a given block of code inside a coroutine with a specified timeout.

Throw exception on timeout

A TimeoutCancellationException is thrown if the timeout is exceeded.

import kotlinx.coroutines.*

fun main() = runBlocking {
    withTimeout(1000L) {
        repeat(30) { i ->
            println("Processing $i ...")
            delay(300L)
        }
    }
}

Throw exception on timeout

Return null on timeout

import kotlinx.coroutines.*

fun main() = runBlocking {
    val status = withTimeoutOrNull(1000L) {
        repeat(30) { i ->
            println("Processing $i ...")
            delay(300L)
        }
        "Finished"
    }
    println("The processing return status is: $status")
}

Return null on timeout

Exception handling

When using launch

By default, the exceptions thrown inside a coroutine started with launch don't require to be handled and are printed instead to the console. They are treated as uncaught exceptions.

However, they still can be handled by using a CoroutineExceptionHandler.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("$exception handled !")
    }
    val job = GlobalScope.launch(handler) {
        throw UnsupportedOperationException()
    }
    job.join()
}

When using async

import kotlinx.coroutines.*

fun main() = runBlocking {
    val deferred = GlobalScope.async {
        // some business logic
        throw UnsupportedOperationException()
    }
    try {
        deferred.await() // The exception is thrown here
        println("Won't be printed")
    } catch (e: UnsupportedOperationException) {
        println("UnsupportedOperationException handled !")
    }
}

Kotlin Coroutines Exception handling

Android usage

Setup

To be able to launch coroutines with the Dispatchers.Main context, you have to add the kotlinx-coroutines-android dependency to your project. For example, when using Gradle, add the following line to your app/build.gradle file inside your dependencies:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1"

Examples

Coroutines are commonly used on View Models to fetch data from a database or from the Internet.

import androidx.lifecycle.*
import kotlinx.coroutines.*

// Android View Model (holds data to be displayed in a view)
class MyViewModel : ViewModel() {
    private val _properties = MutableLiveData<List<MyData>>()
    val properties: LiveData<List<MyData>>
        get() = _properties

    // To be able to cancel launched coroutines (if any)
    private val job = Job()

    // The coroutine runs using the Main (UI) dispatcher
    private val coroutineScope = CoroutineScope(job + Dispatchers.Main)

    init {
        coroutineScope.launch {
            val deferred: Deferred<List<MyData>> = MyService.getPropsAsync()
            _properties.value = deferred.await()
        }
    }

    // Cancel the job when the view model is destroyed
    override fun onCleared() {
        super.onCleared()
        job.cancel()
    }
}