Exception Handling in CoRoutine

Exception Handling in Co-routine :

One of the special features of coroutines in Kotlin is their exception handling mechanism. Unlike traditional threads, coroutines have structured concurrency, which means that exceptions are propagated up the call stack to the parent coroutine, instead of silently crashing the application.

In other words, if a coroutine encounters an exception, it will propagate it up to its parent coroutine or the top-level CoroutineExceptionHandler if no parent is available. This allows for centralized error handling and makes it easier to debug and reason about the flow of execution in the application.

When an exception is thrown in a coroutine, it can be handled in a variety of ways. Two common strategies are "rethrowing the exception" and "propagating it up to its parent coroutine".

Please note that there is difference between "rethrowing the exception" and "propagate it up to its parent coroutine"

In Kotlin coroutines, when an exception is thrown inside a coroutine, there are two ways to handle it:

Rethrowing the exception means that the exception is caught inside the coroutine and then rethrown again using the throw keyword. This allows the exception to be caught and handled by an outer try-catch block or another coroutine.

On the other hand, propagating the exception up to the parent coroutine means that the exception is not caught within the current coroutine i.e no try catch block in the coroutine, but rather it is allowed to propagate up to the coroutine that started the current coroutine. If the parent coroutine is not handling the exception, then the exception will propagate up to the next parent coroutine until it is handled by an outer try-catch block or the top-level coroutine.

There is separate article on above topic i.e rethrowing & propagating.

Exception Re-throw & Propagation in Kotlin co-routine | LinkedIn

Cancellation of coroutines upon exception:

Coroutines have built-in support for cancellation, which can be used to gracefully terminate a coroutine and all of its child coroutines when an exception occurs. This helps to prevent resource leaks and ensures that the application can recover from errors and continue executing as intended.

Overall, the combination of structured concurrency and exception handling in coroutines makes it easier to write robust and error-resistant code, especially in complex and asynchronous applications.

Below are some of the ways to handle exception in Kotlin co-routine

Try-catch block: You can handle exceptions in a try-catch block just like in regular code. When an exception is thrown, it can be caught and handled in the catch block. This is a simple and straightforward way to handle exceptions in coroutines.

GlobalScope.launch { 
    try { 
        // code that can throw an exception 
    } catch (e: Exception) { 
        // handle the exception 
    } 
}         

Example:

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch {
        try {
            // do something that may throw an exception
            println("Inside coroutine")
            throw RuntimeException("Exception in coroutine")
        } catch (e: Exception) {
            // handle the exception here
            println("Caught exception: ${e.message}")
        }
    }
    Thread.sleep(1000) // wait for the coroutine to complete
}
/*
Op => 
Inside coroutine
Caught exception: Exception in coroutine
*/        


In the above example, we launch a coroutine and simulate an exception by throwing a RuntimeException. We catch the exception using a try-catch block and print a message to the console.

  • Advantages: Simple and familiar syntax
  • Disadvantages: Limited to handling exceptions within a single coroutine.

2. CoroutineExceptionHandler: You can define a CoroutineExceptionHandler to handle exceptions in coroutines. The CoroutineExceptionHandler is a handler function that gets called when an exception is thrown. You can set the handler on the CoroutineScope or on individual coroutines using the launch or async functions.

val exceptionHandler = CoroutineExceptionHandler { 
    coroutineContext, throwable -> // handle the exception 
} 

GlobalScope.launch(exceptionHandler) { 
    // code that can throw an exception 
}         

CoroutineExceptionHandler is an interface provided by the kotlinx.coroutines library that allows you to handle uncaught exceptions that occur in coroutines. It provides a way to intercept exceptions and perform some custom actions, such as logging the exception, retrying the operation, or gracefully shutting down the application.

To use CoroutineExceptionHandler, you need to create a class that implements the interface and overrides the handleException function. This function takes two parameters: the coroutine context in which the exception occurred and the exception itself.

Here's an example:

import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext

/*
// A very simple way to create a CoroutineExceptionHandler to handle exceptions
    val simpleHandler = CoroutineExceptionHandler { coroutineContext,
                                                    exception ->
        println("Caught in Handler $exception")
    } 
*/ 
class MyCoroutineExceptionHandler : CoroutineExceptionHandler {
? ? // Define the key property required by the CoroutineExceptionHandler 
    // interface
? ? ?override val key: CoroutineContext.Key<*>
? ? ? ? get() = CoroutineExceptionHandler


? ? // The method to handle the exception
? ? override fun handleException(context: CoroutineContext, exception: Throwable) {
? ? ? ? println("Caught exception: $exception")
? ? }
}


// Define a suspend function that throws an exception after a delay
suspend fun task1() {
? ? println("In Task 1")
? ? delay(1000)
? ? throw RuntimeException("Task 1 failed")
}


// Define another suspend function that throws an exception after a delay
suspend fun task2() {
? ? println("In Task 2")
? ? delay(2000)
? ? throw RuntimeException("Task 2 failed")
}


fun main() = runBlocking {
? ? // Create an instance of our custom exception handler
? ? val handler = MyCoroutineExceptionHandler()

    // A very simple way to create a CoroutineExceptionHandler to handle exceptions
    val simpleHandler = CoroutineExceptionHandler { _, exception ->
        println("Caught in Handler $exception")
    } 

? ? // Launch two coroutines that call our suspend functions and pass 
    // in the exception handler
? ? val job1 = GlobalScope.launch(handler) { 
          // Can use simpleHandler also insed of handler
? ? ? ? task1()
? ? }
? ? val job2 = GlobalScope.launch(handler) {
? ? ? ? task2()
? ? }


? ? // Wait for the coroutines to complete
? ? job1.join()
? ? job2.join()
? ? println("Task 1 and 2 are completed from the main")
}
/*
Op => 
In Task 
In Task 2
Caught exception: java.lang.RuntimeException: Task 1 failed
Caught exception: java.lang.RuntimeException: Task 2 failed
Task 1 and 2 are completed from the main1
*/        

In this example, we define a class MyCoroutineExceptionHandler that implements CoroutineExceptionHandler and overrides the handleException function. When an exception occurs in a coroutine, the handleException function will be called, and it will print the exception message to the console.

We then define two suspend functions task1 and task2, each of which throws a RuntimeException after a delay.

In the main function, we create an instance of our MyCoroutineExceptionHandler class and pass it to the launch function when launching our coroutines. We launch two coroutines, job1 and job2, each of which calls one of our task functions. If an exception occurs in the coroutine, it will be caught by our CoroutineExceptionHandler.

Because the task functions throw exceptions, our CoroutineExceptionHandler will be called for each coroutine. The exception message will be printed to the console, and we can perform any custom actions we want, such as logging the exception.

Finally, we use the join function to wait for the coroutines to complete before exiting the program.

How well does CoroutineExceptionHandler works with async coroutine builder?

When using async with a CoroutineExceptionHandler, any exception thrown in the async block will not be caught by the handler. Instead, you need to handle the exception using a try-catch block around the await() call.

This is because async returns a Deferred object that represents a computation that may fail with an exception. When you call await() on the Deferred object, it will either return the result of the computation or throw an exception if the computation failed.

The CoroutineExceptionHandler only catches exceptions that are thrown in the coroutine scope where it is installed. However, when using async, the actual computation is executed in a separate coroutine scope that is created implicitly by the async function. Therefore, any exceptions thrown in the async block are not caught by the CoroutineExceptionHandler.

Example :

import kotlinx.coroutines.*

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { coroutineContext, exception ->
        println("Caught in Handler: $exception")
    }

    supervisorScope {
        val deferred1 = async(handler) {
            println("Task 1 started")
            delay(1000)
            println("Task 1 completed")
            throw RuntimeException("Task 1 failed")
        }
        val deferred2 = async(handler) {
            println("Task 2 started")
            delay(2000)
            println("Task 2 completed")
        }

        try {
            deferred1.await()
            deferred2.await()
        } catch (ex: Exception) {
            println("Caught in supervisorScope: $ex")
        }
    }
}
/*
Op => 
Task 1 started
Task 2 started
Task 1 completed
Caught in supervisorScope: java.lang.RuntimeException: Task 1 failed
Task 2 completed 
*/        

In the above example, although a CoroutineExceptionHandler is defined, it will have no effect because exceptions thrown by an async block are not propagated to the parent coroutine.

In the given code, if deferred1 throws an exception, it will not be caught by the CoroutineExceptionHandler and will not be propagated to the parent coroutine. Instead, the exception will be caught when we call await() on the Deferred object, and we need to handle it using a try-catch block.

On the other hand, if an exception is thrown by the supervisorScope block, it will be caught by the CoroutineExceptionHandler and handled appropriately.

So, in summary, when launching a coroutine with async and a CoroutineExceptionHandler, any exception thrown by the coroutine will not be propagated to the parent coroutine, and you will need to handle it explicitly with a try-catch block.

  • Advantages: Can handle exceptions from multiple coroutines, easy to set up.
  • Disadvantages: Limited to handling exceptions within the CoroutineScope or coroutine.

3. SupervisorJob: A SupervisorJob is a job that is used to supervise other jobs. If a child job throws an exception, the SupervisorJob will keep running and will not cancel the other child jobs. You can use a SupervisorJob to handle exceptions in coroutines.

val supervisorJob = SupervisorJob() 
val coroutineScope = CoroutineScope(Dispatchers.Default + supervisorJob) 
coroutineScope.launch { 
    // code that can throw an exception 
}        

Working example:

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    val supervisorJob = SupervisorJob()
    val supervisorScope = CoroutineScope(coroutineContext + supervisorJob)

    val job1 = supervisorScope.launch(CoroutineExceptionHandler { _, e ->
        println("Coroutine 1 failed: $e")
    }) {
        delay(100)
        throw RuntimeException("Coroutine 1 failed")
    }

    val job2 = supervisorScope.launch(CoroutineExceptionHandler { _, e ->
        println("Coroutine 2 failed: $e")
    }) {
        delay(2000)
        throw RuntimeException("Coroutine 2 failed")
    }

    supervisorScope.launch {
        job1.join()
        job2.join()
        println("All coroutines completed")
    }
}
/*
// Op => 
Coroutine 1 failed: java.lang.RuntimeException: Coroutine 1 faile
Coroutine 2 failed: java.lang.RuntimeException: Coroutine 2 failed
All coroutines completed        

In the above example, we create a SupervisorJob and use it to create a CoroutineScope. We then launch two child coroutines within this scope, and use a CoroutineExceptionHandler to handle any exceptions that may be thrown by these coroutines. We also launch a third coroutine within the same scope, which waits for the other two coroutines to complete and then prints a message indicating that all coroutines have completed.

The SupervisorJob ensures that if one of the child coroutines fails, the other one can continue executing, and the parent coroutine that waits for both of them will still complete successfully. Additionally, the CoroutineExceptionHandler allows us to handle any exceptions thrown by the child coroutines and take appropriate action, such as logging the error or attempting to recover from it.

  • Advantages: Can handle exceptions in a different context than where they were thrown, easy to set up.
  • Disadvantages: Limited to handling exceptions within a single coroutine.

Note: Please note that supervisorJob and supervisorScope are related concepts in Kotlin coroutines.

supervisorJob is a job that behaves like a supervisor. It means that if any of its child jobs fail with an exception, the supervisor job does not get cancelled and the other child jobs continue to execute. This is different from a regular job, where any exception in its child jobs causes the whole job hierarchy to be cancelled.

supervisorScope is a coroutine builder that creates a new coroutine scope and a new supervisorJob for that scope. Any coroutine launched within the supervisorScope will have the same supervisor as the scope, and therefore will not cancel the whole hierarchy if it throws an exception.

So, supervisorScope provides a way to create a new scope with a supervisor job, which can be useful in cases where you want to limit the scope of the supervisor to a specific part of your code.

  • Advantages: Can handle exceptions from multiple coroutines, easy to set up.
  • Disadvantages: Limited to handling exceptions within the CoroutineScope or coroutine.


4. Flow.catch: If you're using the Flow API, you can handle exceptions using the catch operator. The catch operator returns a new flow that emits a value or throws a new exception when an exception is caught.

In Kotlin coroutines, Flow is a stream of asynchronous data that can be emitted by a producer and collected by a consumer. To handle exceptions that occur during the execution of a Flow and emit a fallback value or perform some other action, you can use the catch operator.

The catch operator is used to catch and handle exceptions that occur during the execution of a Flow. It allows you to emit a fallback value or perform some other action when an exception occurs.

fun getFlow(): Flow<Int> = flow { 
    // code that can throw an exception 
} GlobalScope.launch { 
    getFlow() .catch { 
        e -> // handle the exception 
    } .collect { 
        value -> // process the value 
    } 
}        

Example :

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.io.IOException

fun main() = runBlocking {
    val flow = flow {
        emit(1)
        emit(2)
        throw IOException("Something went wrong!")
        emit(3)
    }

    flow.catch { e ->
        emit(4) // fallback value
        println("Caught exception: $e")
    }.collect {
        println("Value: $it")
    }
}
/*
Op => 
Value: 1
Value: 2
Value: 4
Caught exception: java.io.IOException: Something went wrong!
*/        

Below is RX java style example :

import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.runBlocking


fun main() = runBlocking {
? ? getNumbers()
? ? ? ? .onStart { println("Started emitting numbers...") }
? ? ? ? .onCompletion { println("Finished emitting numbers.") }
? ? ? ? .catch { e -> println("Caught exception: $e") }
? ? ? ?// .onErrorResumeNext { e -> emitAll(getFallbackNumbers()) }? // This depricated?
? ? ? ? .collect { println("Received number: $it") }
}


fun getNumbers(): Flow<Int> = flow {
? ? for (i in 1..5) {
? ? ? ? delay(100)
? ? ? ? emit(i)
? ? ? ? if (i == 3) throw Exception("Error emitting number 3")
? ? }
}


fun getFallbackNumbers(): Flow<Int> = flow {
? ? emit(100)
? ? emit(200)
? ? emit(300)
}
/*
Started emitting numbers...
Received number: 1
Received number: 2
Received number: 3
Finished emitting numbers.
Caught exception: java.lang.Exception: Error emitting number 3
*/        

In this example, we have two functions getNumbers() and getFallbackNumbers() that return a Flow of integers.

getNumbers() emits the numbers 1 through 5 with a delay of 100 milliseconds between each emission. It also throws an exception when it reaches the number 3.

The main() function starts by calling getNumbers() and adding some operators to it.

First, onStart is used to print a message when the flow starts emitting numbers.

Then, onCompletion is used to print a message when the flow has finished emitting numbers.

Next, catch is used to catch any exceptions that are thrown by getNumbers(). In this case, we simply print a message to indicate that an exception has occurred.

The resulting flow is collected using the collect operator, which simply prints each received number.

In the above example, the flow emits three values, but throws an exception after the second value. The catch operator catches the exception, emits a fallback value of 4, and prints a message with the exception.

Note : The onErrorResumeNext operator has been deprecated in favor of the catch operator.

It's important to note that the catch operator should be used only when you want to handle the exception and continue the stream with a fallback value. If you want to propagate the exception and terminate the stream, you can simply not use the catch operator and let the exception propagate to the subscriber.

  • Advantages: Can handle exceptions in a flow, easy to set up.
  • Disadvantages: Limited to handling exceptions within a flow.


Different example of handling the exceptions in Co-routines.

Below are some of the examples for your reference.

Example 1: try-catch with withContext: You can use the withContext function to switch to a different coroutine context and handle exceptions in a try-catch block.

The try-catch block with withContext is used to handle exceptions that may occur within a coroutine, where you need to perform some work in a different context or thread. withContext is used to switch the coroutine context to a different dispatcher or thread while preserving the coroutine stack trace.

Example 1:

import kotlinx.coroutines.*

fun main() {
    runBlocking {
        try {
            val result = withContext(Dispatchers.Default) {
                // some computation that may throw an exception
                1 / 0
            }
            println("Result: $result")
        } catch (e: Exception) {
            println("Caught exception: $e")
        }
    }
}
// Op =>
//Caught exception: java.lang.ArithmeticException: / by zer o        

In this example, we use withContext to run a computation on the Default dispatcher, which is a common thread pool used for CPU-bound tasks. The computation is 1 / 0, which will throw an exception. We enclose this computation within a try-catch block and catch any exception that is thrown. In this case, the catch block will execute and print the caught exception message to the console.

If the computation within the withContext block completes without throwing an exception, the result will be printed to the console.

Example 2:

Using flow and catch:

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val numbers = (1..5).asFlow()
    val result = numbers
        .map { if (it == 3) throw RuntimeException("Error processing element $it") else it }
        .catch { emit(-1) } // handle the error by emitting -1
        .toList() // collect the result into a list
    println(result) // print the result
}
/*
O/p => 
[1, 2, -1]
*/        

Here's how the flow of values is emitted and processed in the code:

  1. The numbers flow is created and emits the values 1 and 2.
  2. The map operator is applied to the numbers flow. It maps each emitted value to its square. So, the flow now emits the values 1 and 4.
  3. The catch operator is applied to the numbers flow. It catches any exceptions that occur in the upstream flow and emits the values of the fallback flow.
  4. The fallback flow is created and emits the value -1.
  5. An exception occurs in the map operator when it tries to square the value -1. The catch operator catches this exception and switches to the fallback flow, which emits the value -1.
  6. The catch operator emits the values of the fallback flow, which is just -1.
  7. The collect operator collects the emitted values and prints them to the console. So, the output is [1, 2, -1].

Thanks for reading till end. , and please comment if you have any suggesation or questions!.

要查看或添加评论,请登录

Amit Nadiger的更多文章

  • Rust modules

    Rust modules

    Referance : Modules - Rust By Example Rust uses a module system to organize and manage code across multiple files and…

  • List of C++ 17 additions

    List of C++ 17 additions

    1. std::variant and std::optional std::variant: A type-safe union that can hold one of several types, useful for…

  • List of C++ 14 additions

    List of C++ 14 additions

    1. Generic lambdas Lambdas can use auto parameters to accept any type.

    6 条评论
  • Passing imp DS(vec,map,set) to function

    Passing imp DS(vec,map,set) to function

    In Rust, we can pass imp data structures such as , , and to functions in different ways, depending on whether you want…

  • Atomics in C++

    Atomics in C++

    The C++11 standard introduced the library, providing a way to perform operations on shared data without explicit…

    1 条评论
  • List of C++ 11 additions

    List of C++ 11 additions

    1. Smart Pointers Types: std::unique_ptr, std::shared_ptr, and std::weak_ptr.

    2 条评论
  • std::lock, std::trylock in C++

    std::lock, std::trylock in C++

    std::lock - cppreference.com Concurrency and synchronization are essential aspects of modern software development.

    3 条评论
  • std::unique_lock,lock_guard, & scoped_lock

    std::unique_lock,lock_guard, & scoped_lock

    C++11 introduced several locking mechanisms to simplify thread synchronization and prevent race conditions. Among them,…

  • Understanding of virtual & final in C++ 11

    Understanding of virtual & final in C++ 11

    C++ provides powerful object-oriented programming features such as polymorphism through virtual functions and control…

  • Importance of Linux kernal in AOSP

    Importance of Linux kernal in AOSP

    The Linux kernel serves as the foundational layer of the Android Open Source Project (AOSP), acting as the bridge…

    1 条评论

社区洞察

其他会员也浏览了