Blog Infos
Author
Published
Topics
,
Published

Photo by Mike Lewis HeadSmart Media on Unsplash

 

This is the third in a series of blog posts about applying Structural concurrency.

Rules of Structured concurrency in Kotlin

Applying Structured concurrency in Kotlin: Part II — Creation

  • (This post) — Applying Kotlin Structured Concurrency: Part III — Exceptions in coroutines

Applying Kotlin Structured concurrency: Part IV — Coroutines Cancellation

In Java and Kotlin you can use try/catch for catch exceptions.

fun main() {   
    try {
        throw (Exception())
    } catch (e: Exception) {
        println(e.javaClass.canonicalName)
    }
}

If you don’t handle an exception in a method where an exception was thrown then you need to handle it in the method that called this method and so on.

fun main() {   
    try {
        funException()
    } catch (e: Exception){
        println(e.javaClass.canonicalName)
    }
}

fun funException(){
    throw (Exception())
}

If there is no such handler in the callstack then user will get an app crash on Android.

Handling exceptions in coroutines work differently. In coroutines we have one more additional way for bubbling exception up — not only re-throw via callstack but also propagation via Job hierarchy. You can read more about Job hierarchy and Job parent-child relations in previous article:

Applying Structured concurrency in Kotlin: Part II — Creation

Structured Concurrency for exception propagation uses the following machinery:

0. Exception is thrown in a child coroutine

  1. Parent cancels other child coroutines. That’s siblings cancellation
  2. Parent coroutine is cancelled too. That’s parent cancellation
  3. Exception is propagated up Job hierarchy

 

Different coroutine builders and suspend functions have differences in their behaviour. Let’s discuss them.

Top-level launch or launch in supervisorScope

Exception thrown here and not caught by try/catch inside launch OR propagated from child leads to:

  1. cancelling other siblings
  2. cancelling itself
  3. propagating up the Job hierarchy

These exceptions can’t be caught outside launch using try/catch because they will not be re-thrown via callstack but will be propagated via Job hierarchy.

import kotlinx.coroutines.*

fun main() {   
  val scope = CoroutineScope(Job())
    try {
        scope.launch {
            throw (Exception())
        }
    } catch (e: Exception) {
        println(e.javaClass.canonicalName)
    }
    Thread.sleep(1000)
}

Exception can be handled on top of Job hierarchy by installed CoroutineExceptionHandler or by inherited CoroutineExceptionHandler.

import kotlinx.coroutines.*

fun main() {   
    val ceh = CoroutineExceptionHandler { context, exception ->
        println("CEH: $exception")
    }
    val scope = CoroutineScope(Job() + ceh)
    
    scope.launch {
        throw (Exception())
    }
    Thread.sleep(1000)
}

If CoroutineExceptionHandler is missing — the exception is passed to the thread’s uncaught exception handler. On Android, it will lead to application crash.

Top-level async or async in supervisorScope

Exception thrown here and not caught by try/catch inside async OR propagated from a child will be encapsulated in Deferred object returned by the builder.

It is thrown as a normal exception only when invoking the await() method. Therefore — await() surrounded by try/catch should be used to avoid crash.

import kotlinx.coroutines.*

fun main() {   
val scope = CoroutineScope(Job())
var deferred: Deferred<Unit>? = null

    scope.launch {
        supervisorScope {
            deferred = async {
                throw (Exception())
            }
        }

        supervisorScope {
            try {
                deferred?.await()
            } catch (e: Exception) {
                println(e.javaClass.canonicalName)
            }
        }
    }
    Thread.sleep(1000)
}

Without await() you can’t catch an exception in async surrounding by try/catch — so exception will be silently dropped.

import kotlinx.coroutines.*

fun main() {   
    val scope = CoroutineScope(Job())

    scope.async {
        throw (Exception())
    }
    Thread.sleep(1000)
}

CoroutineExceptionHandler has no effect here because there is no propagation via Job hierarchy.

Nested launch

Uncaught exception in launchOR exception propagated from child leads to:

  1. cancelling other children
  2. cancelling itself
  3. propagating up the Job hierarchy

These exceptions are not re-thrown but propagate up the Job hierarchy. If nested launch() is wrapped with try/catch then exception can’t be caught.

Nested async

Uncaught exception propagated from child OR occurred in async leads to:

  1. cancelling other children
  2. cancelling itself
  3. propagating up the Job hierarchy

Exception propagates up the Job hierarchy even without calling await() on it.

If await() is wrapped with try/catch then exception can be caught but it ANYWAY propagates via Job hierarchy.

coroutineScope

Uncaught exception in scoping function coroutineScopeOR exception propagated from child leads to:

  1. cancelling other children
  2. cancelling itself
  3. it doesn’t propagate exception up the Job hierarchy

Exception will be re-thrown, it allows to handle exception of failed coroutineScope surrounded by try/catch.

import kotlinx.coroutines.*

fun main() {   
    val scope = CoroutineScope(Job())

    scope.launch {
        try {
            coroutineScope {
                throw (Exception())
            }
        } catch (e: Exception) {
            println(e.javaClass.canonicalName)
        }
    }
    Thread.sleep(1000)
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

supervisorScope (exception thrown in scope)

Uncaught exception in scoping function supervisorScope (exception thrown in the block) leads to:

  1. cancelling other children
  2. cancelling itself
  3. it doesn’t propagate exceptions up the Job hierarchy.

Exception will be re-thrown which allows us to handle it by surrounding supervisorScope with try/catch.

import kotlinx.coroutines.*

fun main() {   
    val scope = CoroutineScope(Job())

    scope.launch {
        try {
            supervisorScope {
                //child
                launch {
                    delay(50)
                    println("sibling")
                }

                throw (Exception())
            }
        } catch (e: Exception) {
            println(e.javaClass.canonicalName)
        }
    }    
    Thread.sleep(1000)
}
supervisorScope (exception thrown in child coroutine)

If one of the child coroutines fails:

  1. the sibling coroutines are not cancelled
  2. supervisorScope is not cancelled
  3. no exception propagation up the Job hierarchy
  4. no re-throw exceptions

 

So surrounding supervisorScope with try/catch doesn’t catch exception and there is no propagation via Job hierarchy.

Coroutines that are started directly from the supervisorScope are top-level coroutines — behaviour of top level launch and async works like follows:

launch child coroutine requires installation of a CoroutineExceptionHandler (or using inherited CoroutineExceptionHandler from CoroutineExceptionHandler of parent Job). If there is no CoroutineExceptionHandler — exception is passed to the thread’s uncaught exception handler (crash on Android).

async/await requires surrounding await with try/catch. Without it — exception is passed to the thread’s uncaught exception handler (crash on Android).

Without await() exception will be silently dropped.

supervisorScope should be used when you have children those errors should propagate only up to a certain point and not all the way to the root.

withContext

Uncaught exception in scoping function withContext OR exception propagated from child leads to:

  1. cancelling other children
  2. cancelling itself
  3. it doesn’t propagate exceptions up the Job hierarchy.

Exception will be re-thrown, it allows to handle exception of failed withContext surrounded by try/catch.

CoroutineExceptionHandler

If you want to use cancellation functionality of Structured Concurrency then you shouldn’t use try/catch inside coroutines.

If an exception in a child coroutine should not stop other coroutines belonging to the same parent, or if you want to retry the operation, or if specific error-handling behaviour is required, try/catch blocks should be used inside coroutine.

Use the CoroutineExceptionHandler for logic that should happen after the coroutine has already been completed.

CoroutineExceptionHandler is a last-resort mechanism for global “catch all” behaviour. You cannot recover from the exception in the CoroutineExceptionHandler(scope is canceled and coroutine can’t be restarted). The coroutine had already completed with the corresponding exception when the handler is called. Normally, the handler is used to log the exception, show some kind of error message, terminate, and/or restart the application.

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

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

Fill out this field
Fill out this field
Please enter a valid email address.

Menu