1. Overview
Continuation is the core of the coroutines mechanism. Understanding how coroutines are suspended and resumed is especially important for developers who want to gain more control over how coroutines work in Kotlin.
In this article, we’ll discuss how Kotlin coroutine Continuation works, along with code examples.
2. Suspending
In the Kotlin context, suspending means a block of code in the form of a function or lambda that can suspend its execution and resume later.
Furthermore, suspend functions use the keyword suspend to mark themselves as functions that can suspend their execution:
private suspend fun doWork(name: String, delay: Long) : String {
logger.info("$name started")
delay(delay)
logger.info("$name resumed")
return name
}
When a coroutine is suspended, that thread is free for other coroutines. The Continuation of the coroutine doesn’t have to be on the same thread:
@Test
fun `prove suspending`() = runBlocking {
val dispatcher = Dispatchers.Default
val job1 = async(dispatcher) {
doWork("Job 1", 2000)
}
val job2 = async(dispatcher) {
doWork("Job 2", 600)
}
val job3 = async(dispatcher) {
doWork("Job 3", 100)
}
assertEquals("Job 1", job1.await())
assertEquals("Job 2", job2.await())
assertEquals("Job 3", job3.await())
logger.info("All coroutines finished!")
}
Let’s see what’s going on behind that in the log:
16:25:58.011 [DefaultDispatcher-worker-3 @coroutine#4] INFO - Job 3 started
16:25:57.989 [DefaultDispatcher-worker-1 @coroutine#2] INFO - Job 1 started
16:25:58.010 [DefaultDispatcher-worker-2 @coroutine#3] INFO - Job 2 started
16:25:58.115 [DefaultDispatcher-worker-2 @coroutine#4] INFO - Job 3 resumed
16:25:58.614 [DefaultDispatcher-worker-2 @coroutine#3] INFO - Job 2 resumed
16:26:00.014 [DefaultDispatcher-worker-2 @coroutine#2] INFO - Job 1 resumed
16:26:00.016 [main @coroutine#1] INFO - All coroutines finished!
We see that job3 is starting to run on the DefaultDispatcher-worker-3 thread. Then, when it resumes, it’s in the DefaultDispatcher-worker-2 thread. Likewise, job2 and job**1 start and continue again and don’t have to be on the same thread as when they started.
3. Continuation
A Continuation is like a finite state machine that saves the current execution state, like the execution stack, program counter, and so on, and manages the execution flow according to callbacks. The suspend function is adapted to accept an additional Continuation parameter to support Continuation Passing Style (CPS).
Further, Continuation
interface Continuation<in T> {
val context: CoroutineContext
fun resumeWith(result: Result<T>)
}
It has one property and one function:
CoroutineContext represents a collection of elements that define the behavior and environment of a coroutine. These elements can be CoroutineName, CoroutineId, CoroutineExceptionHandler, ContinuationIntercepter, CoroutineDispatcher, and Job. Then each element in this set has a unique key.
The resumeWith() function is used to propagate the results in between suspension points; it’s called with the result (or exception) of the last suspension point and resumes the coroutine execution.
3.1. Implicit Continuation When Using suspend
When we call the suspending function, it suspends its execution and returns a Continuation object. Here there is a Continuation object, even though we didn’t create it.
To prove it, we can look at the bytecode of classes where there is a suspending function.
For example in IntelliJ IDEA we can click: Tools -> Kotlin -> Show Kotlin Bytecode -> Decompile.
In this case it means the doWork() in section 2:
private suspend fun doWork(name: String, delay: Long) : String {
// ...
}
Then, we see this function in Bytecode change to something like this:
private final Object doWork(String name, long delay, Continuation $completion) {
// ...
}
We see there is a Continuation $completion even though we didn’t explicitly create it.
This Continuation object stores information about the state of the function and the context in which it’s called.
Therefore, it’s important to understand suspend and Continuation together, because they’re related.
3.2. Create Continuation
However, we can also create Continuation objects without having to create a suspending function:
val simpleContinuation = Continuation<Int>(Dispatchers.IO) { result ->
assertEquals(45, result.getOrNull())
}
simpleContinuation.resumeWith(Result.success(45))
Firstly, we create a Continuation object, Continuation
If simpleContinuation is executed on Dispatchers.IO, resumeWith() won’t immediately run the lambda on the same thread in which it was called, but hands off its execution to a different thread managed by Dispatchers.IO. Thus, resumeWith() on Dispatchers.IO will be asynchronous.
However, if using a dispatcher such as Dispatchers.Unconfined or if not using a dispatcher at all (the default), then resumeWith() will probably be executed synchronously in the same thread.
3.3. Resuming suspendCoroutine
When a coroutine waits for something that takes time, such as reading data from the internet, it’s not actively waiting. Instead, it gives the computer a chance to do other work. When the data is ready, the coroutine continues its work from where it stopped.
We can resume suspendCoroutine that allows us to suspend the execution of a coroutine and manually control its resumption:
val result = suspendCoroutine { continuation ->
continuation.resumeWith(Result.success("Baeldung"))
}
assertEquals("Baeldung", result)
The lambda function takes a single parameter, Continuation, which is of type Continuation
To make it clearer, let’s try a case that is closer to a real project:
private suspend fun usingResumeWith(url: String): String {
return withContext(Dispatchers.IO) {
suspendCoroutine {
continuation - >
val result =
try {
val connection = (URL(url).openConnection() as HttpURLConnection).apply {
requestMethod = "GET"
connectTimeout = 5000
readTimeout = 5000
}
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
Result.success("$responseCode")
} else {
Result.failure(Exception("$responseCode - Failed"))
}
} catch (e: Exception) {
Result.failure(Exception(e.message))
}
continuation.resumeWith(result)
}
}
}
We use withContext(Dispatchers.IO) to switch the coroutine context to the I/O dispatcher, which is optimized for I/O operations such as network requests.
If the response code is HTTP_OK (200), it creates a Result with a success message. This test shows it:
assertEquals("200", usingResumeWith("https://hangga.github.io"))
But if the response code isn’t HTTP_OK, it creates a failure Result with an exception containing a failure message.
Let’s do a test with a valid URL, but the page doesn’t exist in the hope that it throws a 404 error:
val throwed = assertThrows<Exception> {
usingResumeWith("https://hangga.github.io/fail")
}
assertEquals("404 - Failed", throwed.message)
We can see that the assertThrows() function captures the thrown exception, and the subsequent assertEquals() checks if the exception message is “404 – Failed”, confirming that the error handling is as expected.
Then, if an exception occurs during the network request, for example, if it turns out the URL is invalid, it creates a failure Result with an exception containing the error message.
Then we call for continuation.resumeWith(result) to resume the suspended coroutine with the result (either success or failure) obtained from the network request:
val thrown = assertThrows<Exception> {
usingResumeWith("invalid-url")
}
assertEquals("no protocol: invalid-url", thrown.message)
We can see that an exception was thrown with the message we expected.
3.4. Using resume() and resumeWithException()
We can also use resume() and resumeWithException() that extension functions, to resume execution of a suspended coroutine.
Let’s use resume() for successful results and resumeWithException() when there is a potential error and we need to throw an exception:
private suspend fun usingResumeAndResumeWithException(url: String): String {
return withContext(Dispatchers.IO) {
suspendCoroutine {
continuation - >
try {
val connection = (URL(url).openConnection() as HttpURLConnection).apply {
requestMethod = "GET"
connectTimeout = 5000
readTimeout = 5000
}
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
continuation.resume("$responseCode")
} else {
continuation.resumeWithException(Exception("HTTP response code $responseCode - Failed"))
}
} catch (e: Exception) {
continuation.resumeWithException(Exception(e.message))
}
}
}
}
We use continuation.resume() when the responseCode is HTTP_OK(200) so that the coroutine resumes with a successful result. Let’s just do a simple test:
assertEquals("200", usingResumeAndResumeWithException("https://hangga.github.io"))
But if responseCode isn’t HTTP_OK, then we resume the coroutine with the exception using continuation.resumeWithException():
val thrown = assertThrows<Exception> {
usingResumeAndResumeWithException("https://hangga.github.io/fail")
}
assertEquals("HTTP response code 404 - Failed", thrown.message)
If any exception occurs during the network request, for example, if timeout occur, it calls resumeWithException():
val thrown = assertThrows<Exception> {
usingResumeAndResumeWithException("https://10.255.255.1")
}
assertEquals("Connect timed out", thrown.message)
We see that it throws an exception to the message we expected.
4. Conclusion
In this tutorial, we discussed how Kotlin Continuation works. We also saw that it’s important to understand suspending and Continuation because they’re related.
suspendCoroutine allows suspending coroutine execution and manual control over its Continuation, useful for time-consuming operations such as network requests. By default, we can use resumeWith(). However, Continuation also provides extension functions resume() and resumeWithException() which are used to continue coroutine execution with a success or exception result.
As always, the source code for the examples is available over on GitHub.