About 3 years ago, my prediction that Kotlin Coroutine would be the way Network Fetching be. I provided some simple examples in the article.
However, there’s nothing on how we can handle the Error, Exception, and Cancellation. Here in this article, I’m providing the barebone Kotlin Coroutine way of network fetching (without Retrofit, RxJava, or Kotlin-Flow), so we have a good grasp of how it works.
The Example App.
Accompanied with the article, I also provide the example add with code below. The network fetch is doing a keyword fetch for the count of its occurrence in Wikipedia. Check here for detail.
I use both approach of launch
and async-await
approach of fetching.

Recap the basic Coroutine Network Fetching.
Below are the 2 fetching approaches
Launch
In this fetching, we will launch the fetching in another thread. It is like fire-and-forget, and let it handle its own return when it is done.
fun fetchData(searchText: String) {
coroutineScope = CoroutineScope(Dispatchers.IO)
coroutineScope?.launch {
Log.d("Track", "Launch Fetch Started")
val result = Network.fetchHttpResult(searchText)
launch(Dispatchers.Main) {
when(result) {
is Network.Result.NetworkError -> {
view.updateScreen(result.message)
Log.d("Track", "Launch Post Error Result")
}
is Network.Result.NetworkResult -> {
view.updateScreen(result.message)
Log.d("Track", "Launch Post Success Result")
}
}
}
}
}

From the code above, we can see that it first launch a coroutine in a new thread (Dispatchers.IO
), to perform a fetch.
After the fetch is completed, it then launch a launch a new coroutine in the main thread (Dispatchers.Main
), to update the UI with the result.
What’s nice about coroutine is, although the code looks sequential, it is done asynchronously.
Async-await
In this fetching, we will launch a coroutine in the main thread, which will launch the fetch in another thread, and wait for it to finish.
fun fetchData(searchText: String) {
coroutineScope = MainScope()
coroutineScope?.launch {
val defer = async(Dispatchers.IO) {
Log.d("Track", "Async Fetch Started")
Network.fetchHttpResult(searchText).apply {
Log.d("Track", "Async Fetch Done")
}
}
when (val result = defer.await()) {
is Network.Result.NetworkError -> {
view.updateScreen(result.message)
Log.d("Track", "Async Post Error Result")
}
is Network.Result.NetworkResult -> {
view.updateScreen(result.message)
Log.d("Track", "Async Post Success Result")
}
}
}
}

From the code above, we can see that it first launch a coroutine in the main thread (Dispatchers.MAIN
), to launch async
in a new thread (Dispatchers.IO
) to perform the fetch.
While it is fetching, the await
is called to wait for the fetching to finish. Since this is a coroutine, the function is suspended for the original main thread to continue what is it doing, hence not blocking the main thread from the UI work as shown in the diagram above. (white block within the Main Thread Coroutine block)
Coroutine Exception Handling
One of the seeming drawback of Coroutine is error handling compared to RxJava is, it doesn’t seem as clean within the flow.
Try-Catch Flow (not ideal way)
To use a try-catch
, it will look something as below
// Launch approach (NOT-IDEAL)
fun fetchData(searchText: String) {
coroutineScope = CoroutineScope(Dispatchers.IO)
coroutineScope?.launch {
try {
val result = Network.fetchHttpResult(searchText)
launch(Dispatchers.Main) {
try {
when(result) {
is Network.Result.NetworkError -> {
view.updateScreen(result.message)
}
is Network.Result.NetworkResult -> {
view.updateScreen(result.message)
}
}
} catch (e: Exception) {
// Main Thread Coroutine catch
}
}
} catch (e: Exception) {
// IO Thread Coroutine catch
}
}
}
and below
// Asycn-Await approach (NOT-IDEAL)
fun fetchData(searchText: String) {
coroutineScope = MainScope()
coroutineScope?.launch {
try {
val defer = async(Dispatchers.IO) {
try {
Log.d("Track", "Async Fetch Started")
Network.fetchHttpResult(searchText).apply {
Log.d("Track", "Async Fetch Done")
}
} catch (e: Exception) {
// IO Thread Coroutine catch
}
}
when (val result = defer.await()) {
is Network.Result.NetworkError -> {
view.updateScreen(result.message)
Log.d("Track", "Async Post Error Result")
}
is Network.Result.NetworkResult -> {
view.updateScreen(result.message)
Log.d("Track", "Async Post Success Result")
}
}
} catch (e: Exception) {
// Main Thread Coroutine catch
}
}
}
Each of the coroutine, we’ll need to have try-catch
. Messy.
To do this better, fortunately, we have CoroutineExceptionHandler.
Coroutine Exception Handler (better way)
To avoid having to do multiple try-catch
, we can use CoroutineExcpetionHandler
.
val errorHandler = CoroutineExceptionHandler { context, error ->
coroutineScope?.launch(Dispatchers.Main) {
view.updateScreen(error.localizedMessage ?: "")
}
}
This handler will be triggered when an exception is caught in the coroutine (or the child coroutine).
The thread it runs in is the same thread (e.g. XX Thread as per diagram below) that the scope that the handler is assigned into. In order to ensure that the update is on the UI thread only, we just launch
it to Dispatchers.Main
in our case.

Register the CoroutineExcpetionHandler
To register the error handler, just use it as part of CoroutineContext and pass it through the top level launch
as shown below
// Launch approach
fun fetchData(searchText: String) {
coroutineScope = CoroutineScope(SupervisorJob()+Dispatchers.IO)
coroutineScope?.launch(errorHandler) {
Log.d("Track", "Launch Fetch Started")
val result = Network.fetchHttpResult(searchText)
launch(Dispatchers.Main) {
when(result) {
is Network.Result.NetworkError -> {
view.updateScreen(result.message)
Log.d("Track", "Launch Post Error Result")
}
is Network.Result.NetworkResult -> {
view.updateScreen(result.message)
Log.d("Track", "Launch Post Success Result")
}
}
}
}
}
// Async-await approach
fun fetchData(searchText: String) {
coroutineScope = MainScope()
coroutineScope?.launch(errorHandler) {
val defer = async(Dispatchers.IO) {
Log.d("Track", "Async Fetch Started")
Network.fetchHttpResult(searchText).apply {
Log.d("Track", "Async Fetch Done")
}
}
when (val result = defer.await()) {
is Network.Result.NetworkError -> {
view.updateScreen(result.message)
Log.d("Track", "Async Post Error Result")
}
is Network.Result.NetworkResult -> {
view.updateScreen(result.message)
Log.d("Track", "Async Post Success Result")
}
}
}
}
Do note that it is using SupervisorJob()
within the coroutine scope.
CoroutineScope(SupervisorJob() + Dispatchers.IO)
This is to ensure that the exception faced in the child coroutine, will not stop the parent coroutine from continuing. Without the SupervisorJob
, it will not be able to launch
in the CoroutineExceptionHandler
.
For MainScope()
, it is already
CoroutineScope(SupervisorJob() + Dispatchers.Main)
For more info, check out the below links
- StackOverflow
- Exception in Coroutine
- Making Asynchronous Network Call with Kotlin Coroutine in Android
Cancelling Network Call
To cancel the coroutine, one can get a job.cancel()
or scope.cancel()
. In our case, we want to cancel all the jobs within the scope, we’ll use scope.cancel()
.
To detect cancelation is done, we can catch the CancellationException
just to report that cancelation is done. Note that this Exception will not be sent to CoroutineExceptionHandler
.
coroutineScope?.launch(errorHandler) {
try {
// Coroutine running
} catch (e: CancellationException) {
// Cancellation detected
}
}
Unlike some other framework, the Coroutine way of canceling is cooperative. If a coroutine run without any suspension function call e.g. yield
, delay
, etc, trigger to cancel it will not stop it from running.
Refer to the below for an understanding of how Coroutine works.
This then raises the question, if a network call is in progress, and we cancel the coroutine, what happens? Will it get canceled? Should we add yield
in it?
Launch Approach
Let’s look into the Launch Approach.
We can try the code below, you’ll realize the cancelation won’t be caught
// Cancellation won't be caught
coroutineScope?.launch(errorHandler) {
try {
logOut("Launch Fetch Started")
val result = Network.fetchHttpResult(searchText)
logOut("Launch Fetch Done")
launch(Dispatchers.Main) {
when(result) {
is Network.Result.NetworkError -> {
view.updateScreen(result.message)
logOut("Launch Post Error Result")
}
is Network.Result.NetworkResult -> {
view.updateScreen(result.message)
logOut("Launch Post Success Result")
}
}
}
} catch (e: CancellationException) {
logOut("Launch Cancel Result")
}
}
The log output we’ll see is
Launch Fetch Started
Launch Fetch Done
It won’t call launch(Dispatchers.Main)
, as in the launch
function, it will check for cancellation before proceeding. Given the cancellation has been triggered, it will not proceed.
However, in our case, we also won’t get the CancellatinException
. If one code doesn’t need to detect cancellation, this is fine, one can remove the try-catch
as it is not helping.
In our case, as we want to catch the CancellationException
, we can add yield()
function to check for the cancellation before proceed to launch(Dispatchers.Main
). If cancellation is detected in the yield()
suspend function, it will then trigger the CancellationException
.
Hence the below code will do the trick.
coroutineScope?.launch(errorHandler) {
try {
logOut("Launch Fetch Started")
val result = Network.fetchHttpResult(searchText)
logOut("Launch Fetch Done")
yield()
launch(Dispatchers.Main) {
when(result) {
is Network.Result.NetworkError -> {
view.updateScreen(result.message)
logOut("Launch Post Error Result")
}
is Network.Result.NetworkResult -> {
view.updateScreen(result.message)
logOut("Launch Post Success Result")
}
}
}
} catch (e: CancellationException) {
logOut("Launch Cancel Result")
}
}
Note the output log will then be (assuming we cancel the process during fetching)
Launch Fetch Started
Launch Fetch Done
Launch Cancel Result
Note the cancelation check only comes after Launch Fetch Done
, because the cancelation check is not there until then.

Optional launch approach
Other than using yield()
, we can use isActive
flag within the coroutine.
coroutineScope?.launch(errorHandler)
logOut("Launch Fetch Started")
val result = Network.fetchHttpResult(searchText)
logOut("Launch Fetch Done")
if (!isActive) { logOut("Launch Cancel Result") } launch(Dispatchers.Main) {
when(result) {
is Network.Result.NetworkError -> {
view.updateScreen(result.message)
logOut("Launch Post Error Result")
}
is Network.Result.NetworkResult -> {
view.updateScreen(result.message)
logOut("Launch Post Success Result")
}
}
}
}
This will work too. We no longer need to catch the CancellationException
, unless we want to throw that ourselves.
We also don’t need to do as below, since launch
will also check for the active status.
if (!isActive) {
logOut("Launch Cancel Result")
} else {
launch(Dispatchers.Main) { ... }
}
To get the complete status of Job Scope, refer here.
From above, we know that the launch
approach is not ideal for cancellation detection, since cancel won’t be detected until the fetching job is completed.
Let’s look at the async-await
approach.
Await-async Approach
Here, we just use a normal try-catch
approach to capture the CancellationException
.
coroutineScope?.launch(errorHandler) {
try {
val defer = async(Dispatchers.IO) {
logOut("Async Fetch Started")
Network.fetchHttpResult(searchText).apply {
logOut("Async Fetch Done")
}
}
when (val result = defer.await()) {
is Network.Result.NetworkError -> {
view.updateScreen(result.message)
logOut("Async Post Error Result")
}
is Network.Result.NetworkResult -> {
view.updateScreen(result.message)
logOut("Async Post Success Result")
}
}
} catch (e: CancellationException) {
logOut("Async Cancel Result")
}
}
This works. Cancelation is detected immediately. It is caught by the await
suspend function that is waiting for the spawn job to complete, while continuing to check for cancellation.
The log as below.
Async Fetch Started
Asycn Cancel Result
Async Fetch Done
You’ll notice even though the cancellation is detected, it won’t stop the fetching job from completing.
It will perform till completion until Async Fetch Done
is printed. However, it will not call the await
subsequent code, as the await
has stopped all the subsequent process of it since cancellation has been detected.

Although it won’t stop the fetch process, async-await
is still better than launch
approach as it can detect cancellation sooner.
Hopes from the above, you’ll get a taste of how to implement a complete basic network fetching work that handles Error, Exception, and Cancellation as well.