Finding the right way to use Kotlin 1.3 Coroutines

I’m trying to figure out the right way to use Kotlin coroutines in Kotlin 1.3 and it seems there are a lot of ways to do it wrong, even when it all “seems to work”.

I learned today that I have to write my suspending functions like this:

suspend fun get(id: Long): KalahGame = coroutineScope {
    val game = async {
        // ... Do things
    }
    game.await()
}

Instead of like this:

suspend fun get(id: Long): KalahGame {
    val game = async {
        // ... Do things
    }
    return game.await()
}

The difference is quite subtle, I had overlooked the crucial = coroutineScope part at first, and it all appeared to work - until this function threw an error because the given ID was not found.

Then, the next invokation of any other coroutine in the same object always resulted in the same exception being rethrown!

I know it was the same exception over and over again from looking at the line numbers, which didn’t match the method being called.

This was really confusing and it was only by luck that this morning I spotted the difference when re-reading Roman Elizarov’s blogpost on Kotlin structured concurrency.

Now I’m wondering, what else am I doing wrong in my code?

coroutineScope { ... } can be used only with a suspending function it seems.
Is it then safe to use `launch { … } in a non-suspending function, when another coroutine-scope is already active from the caller?

From a launch { ... } block, can I safely call a function defined as:

suspend fun AmazonKinesisFirehoseAsync.putRecordAsyncAwait(putRecordRequest: PutRecordRequest): PutRecordResult = suspendCoroutine { continuation ->

    // Do things
}

Or should I put coroutineScope { ... } around that too?
(In the case of that project I’m calling from the topmost coroutine context, in case it makes any difference).

These are all things which I haven’t managed to pick up from the documentation, and looking at code examples from other people does more harm than good, since most of them are based on the older experimental coroutine library does show one the incorrect, unsafe way of doing things.

It feels to me that the documentation should be clearer and show fuller examples, and should also do a better job of showing people what is wrong vs what is right.

It would be nice also, if it wasn’t so easy to do things horribly wrong with such confusing results…

Perhaps some extra inspections in IntelliJ would help?

–Tim

2 Likes

Anything that can possibly go wrong, will go wrong. (Murphy’s law)

This is a know issue Documentation improvement about async.await cancelling outer scope · Issue #787 · Kotlin/kotlinx.coroutines · GitHub

Maybe and maybe not, different choices target different behaviours.

No, it is not required.

1 Like

This was actually another subtly wrong way to do things even before 1.3 :slight_smile:

You’re supposed to use async only when you’re starting several concurrent coroutines, or at least starting one in the background, then continuing to do something else in the calling one, before eventually calling await(). If you just want to transfer the execution to another dispatcher and await the result, you should write

return withContext(otherDispatcher) { 
    // Do things
}

When you’re doing parallel decomposition using async, then coroutineScope makes sense because it will eagerly cancel everything within the scope if any of the subtasks fails. Since you don’t get any exceptions from a failed async until you await on it, you may get into a situation where you await on one result for a long time while another async has already failed, and your whole computation will fail do to it, but you’ll find that out only when your long await comes back with a result.

2 Likes

Thanks for this useful information!

What has really confused me though, is that my original programming error had the effect of every coroutine invocation (or await()?) throwing the same exception over and over again, that I got on the first invocation of another coroutine in the same scope.

That makes program behaviour really hard to understand.

Altogether this underscores the need for better documentation, and better (and more) code-samples.

Yes, this behavior is definitely unusual, but it’s sort of a workaround for the nasty problem of awaiting on the results of several palallel computations. I posted a more elaborate analysis of its rationale on Stack Overflow.

1 Like

Thanks for your feedback!

It feels to me that the documentation should be clearer and show fuller examples

The coroutines documentation lives on Github: https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/coroutines-guide.md . If you have some ideas how to improve it please create an issue in that project.

Perhaps some extra inspections in IntelliJ would help?

Again, if you have something specific in mind please create a YouTrack issue. By the way, here’s the current list of coroutines-specific inspections.

1 Like

Thanks, I will look at these through the lense of what I currently have learned about coroutines and my struggles to get to that understanding!

But to be fair, that was made harder in part by the number of now-obsolete code samples available online that are based on the experimental coroutine implementation from Kotlin 1.1 (of which I have only used buildSequence() / yield() ).

1 Like

I raised one suggestion for a new inspection:

https://youtrack.jetbrains.com/issue/KT-28379

1 Like

This article seems to offer a very good overview of correct and incorrect ways to use various Kotlin Coroutine primitives: