Open In App

Exception Handling in Kotlin Coroutines

Last Updated : 28 Jan, 2022
Improve
Improve
Like Article
Like
Save
Share
Report

Coroutines are a new way for us to do asynchronous programming in Kotlin in Android. When developing a production-ready app, we want to ensure that all exceptions are handled correctly so that users have a pleasant experience while using our app. In this article, we will discuss how to properly handle exceptions in an Android project when using Kotlin coroutines. If you want to learn how to use Kotlin coroutines, you can start here. This article will be divided into the following sections:

  1. What are some of the exceptions?
  2. How do we deal with exceptions in general?
  3. How do we efficiently handle exceptions in Kotlin Coroutines?

What are some of the exceptions?

Exceptions are unexpected events that occur while a program is being run or performed. The execution is disrupted by exceptions, and the expected flow of the application is not executed. That is why, in order to execute the app’s proper flow, we must handle exceptions in our code.

How do we deal with exceptions in general?

A try-catch block is a general way to handle exceptions in Kotlin. In the try block, we write code that may throw an exception, and if an exception is generated, the exception is caught in the catch block. Let us illustrate with an example:

Kotlin




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


In the preceding code, we are attempting to divide 5 by 0 while also adding two numbers, 2,5. The solution should then be printed in Logcat. When we run the app, we should first get the value in the solution variable and then assign the sum to the additional variable. The values will be printed later in the Log statement. The solution and addition variables are not printed in this case, but the Log statement in the catch block is with an Arithmetic Exception. The reason for this is that no number can be divided by 0. So, when we got the exception, you can see that no step was taken below the first line, and it went straight to the catch block. The preceding code demonstrates how an exception can occur and how we can handle it.

How do we efficiently handle exceptions in Kotlin Coroutines?

Now we’ll look at how we can handle exceptions effectively in our project using Kotlin Coroutines. There are several approaches to dealing with exceptions.

Generic method

Using SupervisorScope and CoroutineExceptionHandler. To illustrate this further, consider retrieving a list of users. We’d have an interface:

Kotlin




interface GfgService {
    @GET("courses")
    suspend fun getCourses(): List<GfgUser>
    @GET("more-courses")
    suspend fun getMoreCourses(): List<GfgUser>
    @GET("error")
    suspend fun getCoursesWithError(): List<GfgUser>
}


We have three different suspend functions here that we can use to get a list of users. If you look closely, you’ll notice that only the first two functions, getUsers() and getMoreUsers(), will return a list, whereas the third function, getUserWithError(), will throw an Exception.

gfg.HttpException: HTTP 404 Not Found

For the sake of clarity, we purposefully designed getUserWithError() to throw an Exception. Now, let’s go over how to properly handle exceptions in our code using Kotlin Coroutines.

1. Generic Approach

Consider the following scenario: we have a ViewModel, TryCatchViewModel (present in the project), and I want to perform my API call in the ViewModel.

Kotlin




class TryCatchViewModel(
    private val gfgUser: GfgUser,
    private val gfgCoursedb: DatabaseHelper
) : ViewModel() {
  
    private val gfg = MutableLiveData<Resource<List<GfgCourseUser>>>()
  
    fun fetchGfg() {
        viewModelScope.launch {
            gfg.postValue(Resource.loading(null))
            try {
                val gfgFromApi = gfgUser.getGfg()
                gfg.postValue(Resource.success(gfgFromApi))
            } catch (e: Exception) {
                gfg.postValue(Resource.error("Something Went Wrong", null))
            }
        }
    }
  
    fun getGfg(): LiveData<Resource<List<GfgCourseUser>>> {
        return gfg
    }
}


Without exception, this will return a list of users in my activity. Let’s say we add an exception to our fetchUsers() function. We will change the code as follows:

Kotlin




fun gfgGfgPro() {
    viewModelScope.launch {
        gfgPro.postValue(Resource.loading(null))
        try {
            val moreGfgProFromApi = apiHelper.getGfgProWithError()
            val gfgProFromApi = apiHelper.getGfgPro()
              
            val allGfgProFromApi = mutableListOf<ApiUser>()
            allGfgProFromApi.addAll(gfgProFromApi)
            allGfgProFromApi.addAll(moreGfgProFromApi)
  
            gfgPro.postValue(Resource.success(allGfgProFromApi))
        } catch (e: Exception) {
            gfgPro.postValue(Resource.error("Aw Snap!", null))
        }
    }
}


Without exception, this will return a list of users in my activity. Let’s say we add an exception to our fetchUsers() function. We will change the code as follows:

Kotlin




fun fetchGfgPro() {
    viewModelScope.launch {
        gfgPro.postValue(Resource.loading(null))
        try {
            val moreGfgProFromApi = try {
                apiHelper.getGfgProWithError()
            } catch (e: Exception) {
                emptyList<ApiGfgGfgUser>()
            }
            val gfgProFromApi = try {
                apiHelper.getGfgPro()
            } catch (e: Exception) {
                emptyList<ApiGfgGfgUser>()
            }
  
            val allGfgProFromApi = mutableListOf<ApiGfgGfgUser>()
            allGfgProFromApi.addAll(gfgProFromApi)
            allGfgProFromApi.addAll(moreGfgProFromApi)
  
            gfgPro.postValue(Resource.success(allGfgProFromApi))
        } catch (e: Exception) {
            gfgPro.postValue(Resource.error("Aw Snap", null))
        }
    }
}


In this case, we’ve added an individual exception to both API calls, so that if one occurs, an empty list is assigned to the variable and the execution continues.

This is an example of task execution in a sequential manner.

2. Making use of CoroutineExceptionHandler

In the preceding example, you can see that we are enclosing our code within a try-catch exception. When working with coroutines, however, we can handle exceptions by using a global coroutine exception handler called CoroutineExceptionHandler.

Kotlin




class ExceptionHandlerViewModel(
    private val gfgServerAPI: GfgServerAPI,
    private val gfgHelperDB: DatabaseHelper
) : ViewModel() {
  
    private val gfgUsers = MutableLiveData<Resource<List<ApiGfgUser>>>()
  
    private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
        gfgUsers.postValue(Resource.error("Aw Snap!", null))
    }
    fun fetchGfgUsers() {
        viewModelScope.launch(exceptionHandler) {
            gfgUsers.postValue(Resource.loading(null))
            val gfgUsersFromApi = gfgServerAPI.getGfgUsers()
            gfgUsers.postValue(Resource.success(gfgUsersFromApi))
        }
    }
    fun getGfgUsers(): LiveData<Resource<List<ApiGfgUser>>> {
        return gfgUsers
    }
}


GeekTip: In this case, we’ve added getUsersWithError(), which will throw an exception and pass the handle to the handler.

Notice that we haven’t used a try-catch block here, and the exception will be handled by CoroutineExceptionHandler, which acts as a global exception handler for coroutine.

3. Employing SupervisorScope

We don’t want the execution of our task to be terminated because of an exception. But, so far, we’ve seen that whenever we encounter an exception, our execution fails and the task is terminated. We’ve already seen how to keep the task running in sequential execution; in this section, we’ll see how to keep the task running in parallel execution. So, before we get started with supervisorScope, let us first understand the issue with parallel execution. Assume we perform a parallel execution, such as:

Kotlin




private fun fetchGfgUsers() {
    viewModelScope.launch {
        gfgUsers.postValue(Resource.loading(null))
        try {
                val gfgUsersWithErrorFromGfgServerDeferred = async { gfgServerHelper.getGfgUsersWithError() }
                val moreGfgUsersFromGfgServerDeferred = async { gfgServerHelper.getMoreGfgUsers() }
                val gfgUsersWithErrorFromGfgServer = gfgUsersWithErrorFromGfgServerDeferred.await()
                val moreGfgUsersFromGfgServer = moreGfgUsersFromGfgServerDeferred.await()
                val allGfgUsersFromGfgServer = mutableListOf<GfgServerGfgUser>()
                allGfgUsersFromGfgServer.addAll(gfgUsersWithErrorFromGfgServer)
                allGfgUsersFromGfgServer.addAll(moreGfgUsersFromGfgServer)
  
                gfgUsers.postValue(Resource.success(allGfgUsersFromGfgServer))
  
        } catch (e: Exception) {
            gfgUsers.postValue(Resource.error(Aw Snap!", null))
        }
    }
}


When getUsersWithError() is called in this case, an exception will be thrown, resulting in the crash of our android application and the termination of our task’s execution. In this execution, we are performing parallel execution within coroutineScope, which is located within the try block. We would receive an exception from getUsersWithError(), and once the exception occurs, the execution would halt and the execution would proceed to the catch block.

GeekTip: When an exception occurs, the task’s execution will be terminated.

As a result, we can use supervisorScope in our task to overcome execution failure. In supervisorScope, we have two jobs running concurrently, one of which will throw an exception but the task will still be completed. In this case, we’re using async, which returns a Deferred that will deliver the result later. So, when we use await() to get the result, we use the try-catch block as an expression, so that if an exception occurs, an empty list is returned, but the execution is completed and we get a list of users.

Conclusion

  1. While not using async, we can use try-catch or the CoroutineExceptionHandler to achieve whatever we want to be based on our use-cases.
  2. When using async, we have two options in addition to try-catch: coroutineScope and supervisorScope.
  3. When using async, use supervisorScope with individual try-catch for each task in addition to the top-level try-catch if you want to continue with other tasks if one or more of them failed.
  4. Use coroutineScope with the top-level try-catch with async when you DO NOT want to continue with other tasks if any of them have failed.


Like Article
Suggest improvement
Share your thoughts in the comments

Similar Reads