🎓 A1Learner
🔍
0 of 28 lessons completed

Kotlin Exceptions and Supervision

A supervisor scope keeps one failing child from cancelling the rest of the work. An exception handler gives a launch failure a place to go instead of ending the program.

A failing launch caught outside runBlocking

The launch child throws inside runBlocking. The failure reaches the caller of runBlocking, and the catch block prints its message.

import kotlinx.coroutines.*

fun main() {
    try {
        runBlocking {
            launch {
                throw IllegalStateException("boom")
            }
        }
    } catch (e: IllegalStateException) {
        println("caught: ${e.message}")
    }
    println("main continues")
}

A failed async child read at await

The bad async child fails inside supervisorScope. Its exception surfaces at await, runCatching reads the message, and the good sibling still delivers its value.

import kotlinx.coroutines.*

fun main() = runBlocking {
    supervisorScope {
        val bad = async<Int> { throw IllegalStateException("bad data") }
        val good = async { "good data" }
        println("bad: ${runCatching { bad.await() }.exceptionOrNull()?.message}")
        println("good: ${good.await()}")
    }
    println("scope survived")
}

A handler for a failing launch child

The handler is passed to launch inside supervisorScope. It prints the failure message, the sibling still runs, and the scope survives.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, e ->
        println("handled: ${e.message}")
    }
    supervisorScope {
        val job = launch(handler) {
            throw IllegalStateException("child failed")
        }
        job.join()
        val other = launch {
            println("sibling still runs")
        }
        other.join()
    }
    println("scope survived")
}

Supervision with healthy children

Supervision changes nothing when no child fails. Each child is joined in turn, so the prints land in a fixed order.

import kotlinx.coroutines.*

fun main() = runBlocking {
    supervisorScope {
        val first = launch {
            println("first child")
        }
        first.join()
        val second = launch {
            println("second child")
        }
        second.join()
    }
    println("both done")
}

Example

import kotlinx.coroutines.*

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, e ->
        println("handled: ${e.message}")
    }
    supervisorScope {
        val job = launch(handler) {
            throw IllegalStateException("upload failed")
        }
        job.join()
        val other = launch {
            println("receipt printed")
        }
        other.join()
    }
    println("scope survived")
}

Key points

  • A failing launch child in a plain scope passes its failure up to the parent.
  • An uncaught launch failure reaches the caller of runBlocking.
  • A try and catch block around runBlocking can read that failure through the exception message.
  • supervisorScope stops a child failure from cancelling its siblings.
  • An async child under a supervisor scope holds its failure until await is called.
  • runCatching around await reads that failure as a value.
  • CoroutineExceptionHandler receives the failure of a launch child that nothing else handles.

In this example, the first launch child throws and the handler prints its message. The join waits for that child to finish. The second child then prints its line, and the supervisor scope ends normally.

Note

A handler passed to runBlocking does not catch a failing launch child.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Kotlin Playground

Exercise

What happens to a sibling when one launch child fails inside supervisorScope?