DEV Community

Cover image for Safely Launch Exception-Ready Coroutines
A.J. Kueterman
A.J. Kueterman

Posted on • Edited on

Safely Launch Exception-Ready Coroutines

Launching suspend functions in Kotlin can be a complicated affair. Managing your CoroutineScope and making sure exceptions are handled properly can be confusing and easy to forget.

In Android, when using the ViewModel or Lifecycle specific scopes this gets much easier. We let the Android system provide a CoroutineScope and manage killing our coroutines when the lifecycle of those things end.

fun getObjectFromNetwork() {
  viewModelScope.launch {
    val response = networkRepository.getObject()
  }
}
Enter fullscreen mode Exit fullscreen mode

However, there are cases when exceptions can be thrown from the CoroutineScope. A real life example I experienced recently was a SocketTimeoutException that was thrown from a Retrofit call I was making using a suspend function. The result is an Android app crash, which is definitely not desired when network calls can result in many different thrown exceptions.

The Kotlin CoroutineExceptionHandler can help us more easily handle exceptions thrown from our Coroutine scope, but it requires us to register the exception handler when we launch a new coroutine so we properly handle nested exceptions.

val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
  // handle thrown exceptions from coroutine scope
  throwable.printStackTrace()
}

fun getObjectFromNetwork() {
  viewModelScope.launch(coroutineExceptionHandler) {
    val response = networkRepository.getObject()
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, while the ViewModel is probably a logical place for handling exceptions in network calls, there are a lot of exceptions that are thrown irregularly from your app that aren't part of the logical flow of a network call. Using Retrofit as an example, most network calls should return a Response with a body() or errorBody() to handle instead of edge-cases where actual exceptions, like a SocketTimeoutException is thrown. It might be worth it to you to abstract this error handling away from your ViewModel to help minimize boiler plate.

To try this out, let's leverage the power of Kotlin extensions to create a safeLaunch method on CoroutineScope that can apply a default CoroutineExceptionHandler.

fun CoroutineScope.safeLaunch(launchBody: suspend () -> Unit): Job {
  val coroutineExceptionHandler = CoroutineExceptionHandler { 
  coroutineContext, throwable ->
    // handle thrown exceptions from coroutine scope
    throwable.printStackTrace()
  }

  return this.launch(coroutineExceptionHandler) { 
    launchBody.invoke() 
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, we can call safeLaunch on any CoroutineScope, like our viewModelScope, to launch a coroutine with this default error handling behavior.

fun getObjectFromNetwork() {
  viewModelScope.safeLaunch {
    val response = networkRepository.getObject()
  }
}
Enter fullscreen mode Exit fullscreen mode

There you have it! A nicely-encapsulated method to launch suspend functions knowing that we won't see random app crashes because of an unhandled Throwable.


If we wanted safeLaunch to be the core CoroutineScope launch method in our app, we can even improve the extension a bit to allow users the flexibility of passing their own CoroutineExceptionHandler instead of using the default.

val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->   
  throwable.printStackTrace()
}

fun CoroutineScope.safeLaunch(
  exceptionHandler: CoroutineExceptionHandler = coroutineExceptionHandler,
  launchBody: suspend () -> Unit
): Job {  
  return this.launch(exceptionHandler) { 
    launchBody.invoke() 
  }
}
Enter fullscreen mode Exit fullscreen mode

The same day I wrote this article, Manuel Vivo and Florina Muntenescu from the Android developer relations team released a really good series on coroutines, including the subject of coroutine exceptions. Check it out if you want to learn more about coroutines and how to manage them.

  1. Intro to Coroutines
  2. Cancelling Coroutines
  3. Exceptions in Coroutines

X-post from ajkueterman.dev

Top comments (2)

Collapse
 
jessefeng profile image
Jingzhe(Jesse) Feng

Is there anyway we can change mutable livedata instead of throwable.printStackTrace()

Collapse
 
robotsquidward profile image
A.J. Kueterman • Edited

If you want to make an extension function on CoroutineScope then you abstract that error handling away from your VM - easier for establishing default exception handling but might not be where you want to encapsulate your logic. The ViewModel might be the ideal place for that logic in which case you wouldn't want to use this safeLaunch extension. Just define your own handler and pass it in to launch.

In your VM:

val error: MutableLiveData<String> = MutableLiveData()

val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
  // handle thrown exceptions from coroutine scope
  error.postValue(throwable.stackTrace.toString())
}

fun getObjectFromNetwork() {
  viewModelScope.launch(coroutineExceptionHandler) {
    val response = networkRepository.getObject()
  }
}