Coroutines are a great new feature of Kotlin which allow you to write asynchronous code in a sequential fashion. As with any new asynchronous solution, there is a learning curve. But not as steep, in my opinion, as that of something like RxJava. However, like RxJava, coroutines have a number of little subtleties that you end up learning for yourself during development time, or tricks that you pick up from others.

Our team has been working with coroutines for a few months now and overall it has been a fantastic experience. I wanted to write down some things we learned along the way which, had we known at the beginning of our journey, would have led to us making some different architectural decisions and reduced some debugging headaches.

If you are picking up coroutines for the first time, I highly recommend thoroughly watching Roman Elizarov’s talks at KotlinConf 2017 and also reading through the main guide on the official GitHub page:

Introduction to Coroutines @ KotlinConf

https://www.youtube.com/watch?v=_hfBv0a09Jc

https://www.youtube.com/watch?v=_hfBv0a09Jc Deep Dive into Coroutines on JVM @ KotlinConf:

https://www.youtube.com/watch?v=YrrUCSi72E8

https://www.youtube.com/watch?v=YrrUCSi72E8 Main GitHub guide:

https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md

The GitHub coroutines guide has been updated multiple times since we started using them, and includes some of the items we’ve learned ourselves as well. Once you get a feel for coroutines, it’s also helpful to read through the issues list as the maintainers have been very active in responding (and adding) to them in a very educational manner.

The List

You Can Convert Executors Into CoroutineDispatchers

Executors are generally used as a way to manage various thread pools. If you are integrating coroutines into an existing app, you may want a way to reuse your existing pools. Luckily, the coroutines dependency adds a handy extension function on Executors for you to convert them to a CoroutineDispatcher:

val executorService = Executors.newFixedThreadPool(100)

val coroutineDispatcher = executorService.asCoroutineDispatcher()



launch (coroutineDispatcher) {

// ...

}

On Android, this is also really useful for converting the AsyncTask thread pool into a dispatcher to allow Espresso tests to wait on your coroutines:

launch (AsyncTask.THREAD_POOL_EXECUTOR.asCoroutineDispatcher()) {

// ...

}

I recommend keeping your preferred dispatcher in something like a Dagger graph so that you can swap them in and out depending on your situation.

You Can Create a “Root” Coroutine Parent

If you are used to RxJava and managing disposables through a single object, there’s also an equivalent in coroutines by creating a blank parent job and using it for a number of coroutines:

val rootParent = Job()



fun foo() {

launch(UI, parent = rootParent) {

// ...

}

}



fun bar() {

launch(CommonPool, parent = rootParent) {

// ...

}

}



fun destroy() { rootParent.cancel() }

When you want to cancel all the downstream children, you can just cancel your root parent and, assuming the children are all cooperatively cancelable, they will all clean themselves up.

The CommonPool Has a Very Limited Size

The CommonPool is currently the “default” dispatcher if you don’t specify one when starting a coroutine. It also wraps a thread pool of size numProcessors-1 (with a minimum of 1), as evidenced by the source code:

private fun createPlainPool(): ExecutorService {

val threadId = AtomicInteger()

return Executors.newFixedThreadPool(defaultParallelism()) {

Thread(it, "CommonPool-worker-${threadId.incrementAndGet()}").apply { isDaemon = true }

}

}



private fun defaultParallelism() = (Runtime.getRuntime().availableProcessors() - 1).coerceAtLeast(1)

Depending on what API levels your Android app supports, this can be a real problem. There are quite a few older device models which are single or dual core, which means the CommonPool is only going to be of size 1. This is also different if you’re used to the AsyncTask thread pool, which has a minimum of 2 threads.

Depending on what your app is doing with coroutines, this can lead to device-specific deadlock problems, which is a major headache to debug.

Luckily, the fix isn’t too bad at all — you can just create your own thread pool when your minimum thread count requirement isn’t met and keep it somewhere centralized. For example, the following will use the CommonPool, except on dual or single core devices, where a separate thread pool of size 2 will be used:

val backgroundPool: CoroutineDispatcher by lazy {

val numProcessors = Runtime.getRuntime().availableProcessors()

when {

numProcessors <= 2 -> newFixedThreadPoolContext(2, "background")

else -> CommonPool

}

}

You can also choose to ditch the CommonPool entirely and use your own at all times. As I mentioned previously, if you keep your preferred coroutine dispatcher somewhere centralized and inject it across your app, it becomes easy to switch out later.

Async Can “Swallow” Exceptions if You’re Not Careful

If an exception is thrown during an async block, the Exception is not actually thrown immediately. Instead, it will be thrown at the time you call await on the Deferred that is returned.

// This won't crash...

val deferred = async {

throw Exception("Something failed")

}



launch {

delay(5000)

// ... until 5 seconds have elapsed and we call await()

deferred.await()

}

If you forget to call await() or it’s in a code branch that is missed, then the exception is effectively swallowed (which may or may not be a bad thing).

The suggestion here is really not to use async unless your specific use case calls for it (or, at least, to be aware of this behavior). The nice thing about coroutines is that you can more-or-less write sequential-asynchronous code, so you usually won’t need to use Future-focused coding.

On the other hand, this may also be a feature that you’re interested in — deferring exception handling until a later time.

You Can Get a Reference to the Surrounding CoroutineContext Inside a Suspend Function

UPDATE: Thanks to Louis CAD for telling me this feature is now available in the Kotlin Standard Library as of 1.2.30!