Exception Handling in Kotlin Coroutines

Coroutines have become a new way for us to do asynchronous programming in Android using Kotlin. When building a production-ready app, we want to handle all our exceptions properly for users to have a smooth experience while using our app.

In this blog, we are going to talk about how we can handle exceptions properly in an android project when we are using Kotlin coroutines in the project. If you want to master Kotlin coroutines, you can learn it from here.

We are going to divide this blog into the following sections,

  • What are the exceptions?
  • How do we handle exceptions in a general way?
  • How do we handle exceptions in Kotlin Coroutines efficiently?

What are the exceptions?

Exceptions are the unexpected events that come up while running or performing any program. Due to exceptions, the execution is disturbed and the expected flow of the application is not executed.

That is why we need to handle the exceptions in our code to execute the proper flow of the app.

How do we handle exceptions in a general way?

A generic way to handle exception in kotlin is to use a try-catch block. Where we write our code which might throw an exception in the try block, and if there is any exception generated, then the exception is caught in the catch block.

Let us understand by example,

try {
    val solution = 5 / 0
    val addition = 2 + 5
    Log.d("MainActivity", solution.toString())
    Log.d("MainActivity", addition.toString())
} catch (e: Exception) {
    Log.e("MainActivity", e.toString())
}

In this above code, we are trying to first divide 5 by 0 and also we want to add two numbers 2,5. Then we want to print the solution in Logcat. When we run the app, the proper flow should be first we get the value in the solution variable and then assign the sum in addition variable. Later, we want to print the values in the Log statement.

But, when we run the app we would see the following output,

E/MainActivity: java.lang.ArithmeticException: divide by zero

Here, the solution and addition variables are not printed but the Log statement in the catch block is printed with an Arithmetic Exception. The reason here is, that any number can't be divided by 0. So, when we got the exception you can see that no step was performed below the first line and it directly went to catch block.

The above code is an example of how an exception occurs and how we can handle it.

How do we handle exceptions in Kotlin Coroutines efficiently?

Now, we are going to discuss how we can handle exception efficiently while using Kotlin Coroutines in our project. There are the following ways to handle exceptions,

  • Generic way
  • Using CoroutineExceptionHandler
  • Using SupervisorScope

To discuss this further, we will use an example of fetching a list of users. We would have an interface,

interface ApiService {

    @GET("users")
    suspend fun getUsers(): List<ApiUser>

    @GET("more-users")
    suspend fun getMoreUsers(): List<ApiUser>

    @GET("error")
    suspend fun getUsersWithError(): List<ApiUser>

}

The code has been taken from this project, for complete implementation, you should check the project.

Here, we have three different suspend functions that we would use to fetch a list of users. If you notice, here only the first two functions i.e. getUsers() and getMoreUsers() will return a list but the third function, getUserWithError() will throw an Exception,

retrofit2.HttpException: HTTP 404 Not Found

We have intentionally created the getUserWithError() to throw an Exception for the sake of understanding.

Now, let's discuss the ways to handle the exception using Kotlin Coroutines properly in our code.

1. Generic Way

Let us consider we have a ViewModel, TryCatchViewModel (present in the project) and I want to do my API call in the ViewModel.

class TryCatchViewModel(
    private val apiHelper: ApiHelper,
    private val dbHelper: DatabaseHelper
) : ViewModel() {

    private val users = MutableLiveData<Resource<List<ApiUser>>>()

    fun fetchUsers() {
        viewModelScope.launch {
            users.postValue(Resource.loading(null))
            try {
                val usersFromApi = apiHelper.getUsers()
                users.postValue(Resource.success(usersFromApi))
            } catch (e: Exception) {
                users.postValue(Resource.error("Something Went Wrong", null))
            }
        }
    }

    fun getUsers(): LiveData<Resource<List<ApiUser>>> {
        return users
    }
}

This will return a list of users in my activity without any exception. Now, let's say we introduce an exception in our fetchUsers() function. We will modify the code like,

fun fetchUsers() {
    viewModelScope.launch {
        users.postValue(Resource.loading(null))
        try {
            val moreUsersFromApi = apiHelper.getUsersWithError()
            val usersFromApi = apiHelper.getUsers()
            
            val allUsersFromApi = mutableListOf<ApiUser>()
            allUsersFromApi.addAll(usersFromApi)
            allUsersFromApi.addAll(moreUsersFromApi)

            users.postValue(Resource.success(allUsersFromApi))
        } catch (e: Exception) {
            users.postValue(Resource.error("Something Went Wrong", null))
        }
    }
}

Here, you can see that we are fetching users from two different sources and now the flow of execution should be, first, we first get the output from apiHelper.getUsersWithError() and then from apiHelper.getUsers()

Now, both should be added to the mutableList and then we change the livedata by adding the list to it.

We have all of this code inside the try-catch block to handle the exception we might get. If we don't use a try-catch block then our app would crash when an exception occurs.

But, when we run this, we directly jump to the catch block as we get the 404 Not found exception. So, what happened there,

We the execution reached the getUsersWithError() it got the exception, and it terminated the execution at that point and went to catch block directly.

If you see in the above image, you would see that, if in the scope any of the children get an exception then the following children will not be executed and the execution would be terminated.

Now, let's say we want the execution to still continue even when we get an exception. Then we would have to update the code with individual try-catch block like,

fun fetchUsers() {
    viewModelScope.launch {
        users.postValue(Resource.loading(null))
        try {
            val moreUsersFromApi = try {
                apiHelper.getUsersWithError()
            } catch (e: Exception) {
                emptyList<ApiUser>()
            }
            val usersFromApi = try {
                apiHelper.getUsers()
            } catch (e: Exception) {
                emptyList<ApiUser>()
            }

            val allUsersFromApi = mutableListOf<ApiUser>()
            allUsersFromApi.addAll(usersFromApi)
            allUsersFromApi.addAll(moreUsersFromApi)

            users.postValue(Resource.success(allUsersFromApi))
        } catch (e: Exception) {
            users.postValue(Resource.error("Something Went Wrong", null))
        }
    }
}

Here, we have added an individual exception to both API calls so that, if there is an exception, then an empty list is assigned to the variable and the execution continues.

This is an example when we are executing the tasks in a sequential manner.

2. Using CoroutineExceptionHandler

If you consider the above example, you can see we are wrapping our code inside a try-catch exception. But, when we are working with coroutines we can handle an exception using a global coroutine exception handler called CoroutineExceptionHandler.

To use it, first, we create an exception handler in our ViewModel,

private val exceptionHandler = CoroutineExceptionHandler {   context, exception ->
    )
}

and then we attach the handler to the ViewModelScope.

So, our code looks like,

class ExceptionHandlerViewModel(
    private val apiHelper: ApiHelper,
    private val dbHelper: DatabaseHelper
) : ViewModel() {

    private val users = MutableLiveData<Resource<List<ApiUser>>>()

    private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
        users.postValue(Resource.error("Something Went Wrong", null))
    }

    fun fetchUsers() {
        viewModelScope.launch(exceptionHandler) {
            users.postValue(Resource.loading(null))
            val usersFromApi = apiHelper.getUsers()
            users.postValue(Resource.success(usersFromApi))
        }
    }

    fun getUsers(): LiveData<Resource<List<ApiUser>>> {
        return users
    }

}

Here, we have created and exceptionHandler and attached it to the coroutine. So, now let's say we introduce an exception in fetchUsers() function.

fun fetchUsers() {
    viewModelScope.launch(exceptionHandler) {
        users.postValue(Resource.loading(null))
        
        val usersFromApi = apiHelper.getUsers()
        val moreUsersFromApi = apiHelper.getUsersWithError()
        
        val allUsersFromApi = mutableListOf<ApiUser>()
        allUsersFromApi.addAll(usersFromApi)
        allUsersFromApi.addAll(moreUsersFromApi)
        
        users.postValue(Resource.success(usersFromApi))
    }
}

Here, we have added getUsersWithError() and it will throw an exception and handle will be given to the handler.

Now, notice that here we have not added a try-catch block and the exception would be handled in CoroutineExceptionHandler as it works as a global exception handler for coroutine.

3. Using SupervisorScope

When we get an exception, we don't want the execution of our task to be terminated. But, till now what we have seen is that whenever we get an exception our execution fails and the task is terminated.

Till now, we saw how to keep the task going in sequential execution, in this section we are going to see how the task keeps executing in parallel execution.

So, before starting with supervisorScope, let us understand the problem with parallel execution,

Let's say we do a parallel execution like,

private fun fetchUsers() {
    viewModelScope.launch {
        users.postValue(Resource.loading(null))
        try {
                val usersWithErrorFromApiDeferred = async { apiHelper.getUsersWithError() }
                val moreUsersFromApiDeferred = async { apiHelper.getMoreUsers() }

                val usersWithErrorFromApi = usersWithErrorFromApiDeferred.await()
                val moreUsersFromApi = moreUsersFromApiDeferred.await()

                val allUsersFromApi = mutableListOf<ApiUser>()
                allUsersFromApi.addAll(usersWithErrorFromApi)
                allUsersFromApi.addAll(moreUsersFromApi)

                users.postValue(Resource.success(allUsersFromApi))

        } catch (e: Exception) {
            users.postValue(Resource.error("Something Went Wrong", null))
        }
    }
}

Here, an exception will occur when getUsersWithError() is called and this will lead to the crash of our android application and the execution of our task would be terminated.

To overcome the crash of our application, we would run the code inside coroutineScope so that, if the exception occurs the app would not crash. So, the code would look like,

private fun fetchUsers() {
    viewModelScope.launch {
        users.postValue(Resource.loading(null))
        try {
         
            coroutineScope {
                val usersWithErrorFromApiDeferred = async { apiHelper.getUsersWithError() }
                val moreUsersFromApiDeferred = async { apiHelper.getMoreUsers() }

                val usersWithErrorFromApi = usersWithErrorFromApiDeferred.await()
                val moreUsersFromApi = moreUsersFromApiDeferred.await()

                val allUsersFromApi = mutableListOf<ApiUser>()
                allUsersFromApi.addAll(usersWithErrorFromApi)
                allUsersFromApi.addAll(moreUsersFromApi)

                users.postValue(Resource.success(allUsersFromApi))
            }
        } catch (e: Exception) {
            users.postValue(Resource.error("Something Went Wrong", null))
        }
    }
}

Here, in this, execution where we are doing parallel execution inside coroutineScope, which is inside the try block. We would get an exception from getUsersWithError() and once the exception occurs the execution will stop and it will move to the catch block.

The execution of the task will stop when an exception has occurred.

So, to overcome execution failure we can use supervisorScope in our task.

So, while using supervisorScope, when any one of the children throws an exception, then the other children would keep on executing.

Let's consider the example, in our fetchUsers() function,

fun fetchUsers() {
    viewModelScope.launch {
        users.postValue(Resource.loading(null))
        try {
          
            supervisorScope {
                val usersFromApiDeferred = async { apiHelper.getUsersWithError() }
                val moreUsersFromApiDeferred = async { apiHelper.getMoreUsers() }

                val usersFromApi = try {
                    usersFromApiDeferred.await()
                } catch (e: Exception) {
                    emptyList<ApiUser>()
                }

                val moreUsersFromApi = try {
                    moreUsersFromApiDeferred.await()
                } catch (e: Exception) {
                    emptyList<ApiUser>()
                }

                val allUsersFromApi = mutableListOf<ApiUser>()
                allUsersFromApi.addAll(usersFromApi)
                allUsersFromApi.addAll(moreUsersFromApi)

                users.postValue(Resource.success(allUsersFromApi))
            }
        } catch (e: Exception) {
            users.postValue(Resource.error("Something Went Wrong", null))
        }
    }
}

In the above code, we have getUsersWithError(), and as we till now know that it will throw an error.

Here, in supervisorScope we have two jobs running parallel in which one child will throw an exception but the execution of the task will still happen.

We are using async here, which returns a Deferred that will deliver the result later. So, when we get the result using await(), we are using the try-catch block as an expression so that if an exception occurs, an empty list is returned, but the execution will be completed and we would get a list of users. The output would be,

So, this is how we can handle exceptions and still keep the execution going.

Conclusion:

  • While NOT using async, we can go ahead with the try-catch or the CoroutineExceptionHandler and achieve anything based on our use-cases.
  • While using async, in addition to try-catch, we have two options: coroutineScope and supervisorScope.
  • With async, use supervisorScope with the individual try-catch for each task in addition to the top-level try-catch, when you want to continue with other tasks if one or some of them have failed.
  • With async, use coroutineScope with the top-level try-catch, when you do NOT want to continue with other tasks if any of them have failed.
The major difference is that a coroutineScope will cancel whenever any of its children fail. If we want to continue with the other tasks even when one fails, we go with the supervisorScope. A supervisorScope won’t cancel other children when one of them fails.

This is how we can handle an exception efficiently while using Kotlin Coroutines in our project. We should always handle exceptions properly in our project to help our users have a smoother experience using our product.

You can find the complete project here.

Happy learning.

Team MindOrks :)

Also, Let’s connect on Twitter, Linkedin, Github, and Facebook