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>
}
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
that will deliver the result later. So, when we get the result using
Deferred
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 thetry-catch
or theCoroutineExceptionHandler
and achieve anything based on our use-cases. -
While using
async
, in addition totry-catch
, we have two options:coroutineScope
andsupervisorScope
. -
With
async
, usesupervisorScope
with the individualtry-catch
for each task in addition to the top-leveltry-catch
, when you want to continue with other tasks if one or some of them have failed. -
With
async
, usecoroutineScope
with the top-leveltry-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 :)