Mallow's Blog

Surfing on how to handle in Exceptions in Coroutines?

In the previous blog we have seen coroutines cancellations, and here is the continuation of Kotlin Coroutines.

What if coroutines failed?

When a coroutine fails with an exception, it will get canceled   along with its children, then propagate the exception up to its parent. The exception will reach the root of the hierarchy and all the coroutines of the CoroutineScope started will get cancelled too. To handle this case, you can use a different implementation of Job, i.e., SupervisorJob in the CoroutineContext of the CoroutineScope that creates these coroutines.

SupervisorJob:

When using a SupervisorJob, the failure of a child coroutine doesn’t affect other children. A SupervisorJob won’t cancel either itself and the rest of its children.

You can create a CoroutineScope as like this,

 val uiScope = CoroutineScope(SupervisorJob()) 

If the exception is not handled and the CoroutineContext doesn’t have a CoroutineExceptionHandler, it will reach the default thread’s ExceptionHandler. In the JVM, the exception will be logged to console and in Android, it will make your app crash regardless of the Dispatcher this happens on.

You should use a SupervisorJob or supervisorScope when the failure of child coroutine doesn’t cancel the parent and its siblings.

How to handle the exception?

Coroutines will use the regular Kotlin syntax to handle the exceptions. (i.e.) try/catch or built-in helper functions like runCatching. As we said before those uncaught exceptions will always be thrown. However, different coroutines builder will treat the exceptions in many ways.

Launch

Using launch, you can wrap the code that can throw exceptions inside a try/catch. Here the exceptions will be thrown as soon as they happen.

scope.launch {
	     try {
	         throw RuntimeException()
	     } catch(e: Exception) {
	         // Handle exception
	     }
	 }

Async

When async is used, the exceptions are not thrown automatically, instead they are thrown when you call .await() function. So you can wrap the .await() call inside a try/catch to handle exceptions thrown in async,

supervisorScope {
	     val deferred = async {
	      	   throw RuntimeException()
	     }     
	     try {
           	 deferred.await()
       	     } catch(e: Exception) {
          	// Handle exception thrown in async
      	     }
	}

Here we are using a supervisorScope to call async and await. As I said before, a SupervisorJob lets the coroutine handle the exception that will automatically propagate it up in the hierarchy so the catch block won’t be called.

coroutineScope {
	     try {
	         val deferred = async {
		        throw RuntimeException()
	         }
	         deferred.await()
	     } catch(e: Exception) {
	         // Exception thrown in async won’t be caught here
	         // but propagated up to the scope
	     }
	 }

Exceptions thrown in a coroutineScope builder or in coroutines created by the other coroutines won’t be caught in a try/catch.

CoroutineExceptionHandler:

The CoroutineExceptionHandler is an optional element of a CoroutineContext and it allows you to handle uncaught exceptions. Here how you can define a CoroutineExceptionHandler, at whatever point an exception is caught, you have information about the CoroutineContext where the exception occurred and over the exception itself.

val exceptionHandler = CoroutineExceptionHandler {
	     context, exception -> println("Caught $exception") 
	}

In the below example, the exception will be caught by the exceptionHandler,

val scope = CoroutineScope(Job())
	scope.launch(exceptionHandler) {
	     launch {
	         throw Exception("Failed coroutine") 
	    }
	}

In this other case the handler is installed in a inner coroutine:

val scope = CoroutineScope(Job())
	scope.launch {
	     launch(exceptionHandler) {
	         throw Exception("Failed coroutine")
	     }
}

The exception won’t be caught because the handler is not installed in the right CoroutineContext. The inner launch will propagate the exception up to their parent, since the parent doesn’t know anything about the exceptionHandler, then the exception will be thrown.

Coroutine, without cancellation:

However, there are some cases when you want to complete the operation even after the user navigated away from a screen. In that case, you don’t want the work to be cancelled (e.g. downloading a list of files from the server). Coroutines will run until your application process is alive. In Android, you can use WorkManager to run the operations that should outlive the process (e.g. sending logs to your remote server). To do this, create your own scope in the Application class and call those operations in the coroutines started by it.

You can keep it as applicationScope and it must contain a SupervisorJob() so that failures in coroutines won’t propagate in the hierarchy.

class MyApplication : Application() {

  val applicationScope = CoroutineScope(SupervisorJob() + otherConfig)

}

We don’t need to cancel this scope since it remains active as long as the application process is alive, so we don’t hold a reference to the SupervisorJob. We can use this applicationScope to run coroutines that need a longer lifetime than calling the scope might offer in our app.

You need to start a new coroutine using either launch or async, depending on the non-cancelling operation’s behaviour;

Using launch,

suspend fun downloadFiles() {
	     withContext(Dispatcher.IO) {
	        getFiles()
	        externalScope.launch {
	          downloadFiles()
	       }.join()
	     }
	 }

Using async,

suspend fun downloadFiles():List<File> {
	     withContext(Dispatcher.IO) {
	        getFiles()
	        return externalScope.async {
	          downloadFiles()
	       }.await()
	     }
	 }

Conclusion:

Make sure to utilise SupervisorJob when you need to avoid propagating cancellation when an exception happens, and Job in any case. Handling the exceptions effortlessly in your application is important to have a good user experience, even when things don’t go as expected. Hope this blog will helpful to understand about Cancellation & Exception handling. Will see you on another interesting topic!

Sasikumar Android Team

Mallow Technologies Pvt Ltd

Leave a Comment

Your email address will not be published. Required fields are marked *