Coroutine Exception Handling in Kotlin

Mukesh RajputMukesh Rajput
3 min read

Understanding Exceptions in Coroutines

Exceptions in coroutines are handled a little differently compared to traditional try-catch blocks. The coroutine context and its builders play a significant role in determining how exceptions are handled.

Key Concepts:

  1. CoroutineScope: Manages coroutines and their lifecycles.

  2. CoroutineContext: Holds coroutine-related data such as jobs, dispatchers, and exception handlers.

Types of Coroutine Builders

Before we jump into exception handling, it’s important to understand the two types of coroutine builders:

  • launch: This creates a coroutine that does not return a result but can throw an exception.

  • async: This creates a coroutine that returns a result (through Deferred), and exceptions can be accessed via .await().

How Exceptions Propagate

  • launch: When an exception occurs in a launch coroutine, it gets thrown immediately. This means that you can handle exceptions using try-catch around the coroutine.

  • async: The exception is deferred until you call .await(). It’s only when you try to access the result that the exception is thrown.

Example: Exception Handling in launch

Let’s look at a simple example where we handle exceptions in a launch coroutine.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        try {
            // Simulate a task that throws an exception
            throw ArithmeticException("Divide by zero error!")
        } catch (e: Exception) {
            println("Caught an exception: ${e.message}")
        }
    }
    job.join() // Wait for the coroutine to finish
}

In this example, we use a try-catch block inside the coroutine to handle the exception. When the ArithmeticException is thrown, it gets caught, and we log the error message.

Example: Exception Handling in async

When using async, exceptions are thrown when you call .await().

import kotlinx.coroutines.*

fun main() = runBlocking {
    val deferred = async {
        // Simulate a task that throws an exception
        throw ArithmeticException("Division error!")
    }
    try {
        deferred.await() // This is where the exception is thrown
    } catch (e: Exception) {
        println("Caught an exception: ${e.message}")
    }
}

Here, the async block throws an exception, but it is caught when calling .await(). This gives you control over when to handle the exception, which can be useful in scenarios where you are working with multiple deferred results.

Custom Coroutine Exception Handling with CoroutineExceptionHandler

Kotlin provides a special CoroutineExceptionHandler that allows you to handle exceptions globally for a coroutine scope. This is particularly useful when you don’t want to wrap every coroutine block in try-catch.

import kotlinx.coroutines.*

fun main() = runBlocking {
    // Define a global exception handler
    val handler = CoroutineExceptionHandler { _, exception ->
        println("Caught by CoroutineExceptionHandler: ${exception.message}")
    }
    // Launch a coroutine with the exception handler
    val job = launch(handler) {
        // Simulate a task that throws an exception
        throw ArithmeticException("Oops, an error!")
    }
    job.join()
}

In this example, the CoroutineExceptionHandler catches any uncaught exceptions thrown within the coroutine. Instead of using try-catch inside the coroutine, we apply a global handler to the coroutine’s scope, making the code cleaner.

Summary of Key Points

  • launch: Exceptions are thrown immediately, and you can use try-catch blocks to handle them.

  • async: Exceptions are thrown when calling .await(), and handling them is delayed until you access the result.

  • CoroutineExceptionHandler: Provides a global way to handle exceptions for coroutines, helping you avoid using try-catch everywhere.

Conclusion

Exception handling in coroutines may seem tricky at first, but once you understand how exceptions propagate with launch and async, and how to use CoroutineExceptionHandler, you’ll find it easier to write safe and reliable code.

By using the right strategy for your coroutines, you can prevent your apps from crashing unexpectedly and provide better error handling for smoother user experiences.

0
Subscribe to my newsletter

Read articles from Mukesh Rajput directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mukesh Rajput
Mukesh Rajput

Specializing in creating scalable and maintainable applications using MVVM and Clean Architecture principles. With expertise in Ktor, Retrofit, RxJava, View Binding, Data Binding, Hilt, Koin, Coroutines, Room, Realm, and Firebase, I am committed to delivering high-quality mobile solutions that provide seamless user experiences. I thrive on new challenges and am constantly seeking opportunities to innovate in the Android development space.