티스토리 뷰

Android/Coroutine

Android Coroutine - Exception

강태종 2022. 1. 16. 03:58

Exception

Coroutine에서 기본적으로 Exception 처리 방식은 전달(Propagation)과 노출(Expose)가 있습니다. launchactor같은 빌더는 예외가 부모 Coroutine으로 전달되고, asyncproduce같은 빌더는 결과 값을 사용할 때 Exception이 노출됩니다.

import kotlinx.coroutines.*

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
    val job = GlobalScope.launch { // root coroutine with launch
        println("Throwing exception from launch")
        throw IndexOutOfBoundsException() // Will be printed to the console by Thread.defaultUncaughtExceptionHandler
    }
    job.join()
    println("Joined failed job")
    val deferred = GlobalScope.async { // root coroutine with async
        println("Throwing exception from async")
        throw ArithmeticException() // Nothing is printed, relying on user to call await
    }
    try {
        deferred.await()
        println("Unreached")
    } catch (e: ArithmeticException) {
        println("Caught ArithmeticException")
    }
}
Throwing exception from launch
Exception in thread "DefaultDispatcher-worker-2 @coroutine#2" java.lang.IndexOutOfBoundsException
Joined failed job
Throwing exception from async
Caught ArithmeticException

 

실제로 launch 빌더로 생성된 Coroutine은 Exception을 바로 발생시키지만, async 빌더로 생성된 Coroutine은 await()을 만나야 Exception을 발생시킵니다.


 

CoroutineExceptionHandler

예외가 발생할 수 있는 코드에 try~catch를 직접 설정할 수 있지만 모든 Coroutine에 코드를 작성하면 중복 코드가 많아 질 수 있습니다. 이럴 때 CoroutineExceptionHandler를 사용하여 처리할 수 있습니다. Scope를 생성할 때 Context에 Handler를 추가하면 됩니다.

 

* Thread.uncaughtExceptionHandler 방식과 비슷합니다.

fun exceptionHandler() {
    val exceptionHandler = CoroutineExceptionHandler { context, exception ->
        Log.d("ExceptionHandler", "Exception : $exception", exception)
    }

    CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
        throw Exception("Hello World")
    }
}

 

Propagation vs Exposed

CoroutineExceptionHandler를 만들고 JobDeferred를 Handling 해봅시다. 결과는 Job은 CoroutineExceptionHandler가 처리하지만 Deferred는 처리하지 않습니다. 그 이유는 Deferred는 await()을 통해 Exception을 노출해야 받기 때문입니다.

val handler = CoroutineExceptionHandler { _, exception -> 
    println("CoroutineExceptionHandler got $exception") 
}
val job = GlobalScope.launch(handler) { // root coroutine, running in GlobalScope
    throw AssertionError()
}
val deferred = GlobalScope.async(handler) { // also root, but async instead of launch
    throw ArithmeticException() // Nothing will be printed, relying on user to call deferred.await()
}
joinAll(job, deferred)
CoroutineExceptionHandler got java.lang.AssertionError

 

CancellationException

취소는 Exception과 관련이 있습니다. Coroutine은 내부적으로 cancel()할 때 CancellationException을 발생시킵니다. 하지만 이 ExceptionHandler에 전달되지 않으며 부모에게도 전파하지 않습니다.

 

* try-catch문으로 Exception을 catch 할 수 있습니다.

val handler = CoroutineExceptionHandler { _, exception -> 
    println("CoroutineExceptionHandler got $exception") 
}
val job = GlobalScope.launch(handler) {
    launch { // the first child
        try {
            delay(Long.MAX_VALUE)
        } finally {
            withContext(NonCancellable) {
                println("Children are cancelled, but exception is not handled until all children terminate")
                delay(100)
                println("The first child finished its non cancellable block")
            }
        }
    }
    launch { // the second child
        delay(10)
        println("Second child throws an exception")
        throw ArithmeticException()
    }
}
job.join()
Second child throws an exception
Children are cancelled, but exception is not handled until all children terminate
The first child finished its non cancellable block
CoroutineExceptionHandler got java.lang.ArithmeticException

 

만약 CancellationException이 부모나 형제 Coroutine에 영향을 주어 Exception을 전파하거나 취소시키면  본래 Coroutine Cancel의 의미를 잃어버립니다.


 

Exception Handle

여러개의 Child를 가진 Coroutine에서 Exception이 발생하면 처음으로 발생한 Exception을 처리하게 된다. 만약 여러 Exception이 발생하면 Suppressed Exception형식으로 전달됩니다. (CancellationException은 무시된다.)

Coroutine에서 취소를 제외한 Exception이 발생하면 부모의 Coroutine을 취소시키고, CoroutineExceptionHandler를 사용해도 막을 수 없습니다.

import kotlinx.coroutines.*
import java.io.*

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception with suppressed ${exception.suppressed.contentToString()}")
    }
    val job = GlobalScope.launch(handler) {
        launch {
            try {
                delay(Long.MAX_VALUE) // it gets cancelled when another sibling fails with IOException
            } finally {
                throw ArithmeticException() // the second exception
            }
        }
        launch {
            delay(100)
            throw IOException() // the first exception
        }
        delay(Long.MAX_VALUE)
    }
    job.join()  
}
CoroutineExceptionHandler got java.io.IOException with suppressed [java.lang.ArithmeticException]

'Android > Coroutine' 카테고리의 다른 글

Android Coroutine - 동기화  (0) 2022.01.19
Android Coroutine - Supervision  (0) 2022.01.16
Android Coroutine - Job Lifecycle  (0) 2022.01.15
Android Coroutine - Cancel  (0) 2022.01.08
Android Coroutine - Coroutine Builder  (0) 2022.01.07
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/07   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함