Catch Me If You Can

TL;DR

All errors go through platformExceptionHandlers before hitting UEH. On Android, CEH is added to platformExceptionHandlers to bypass a bug on Android 8. If CEH from platformExceptionHandlers throws ExceptionSuccessfullyProcessed, the error will not hit UEH.

The following abbreviations and terms will be used in the text:

UEH – uncaught exception handler. The essence of the JVM thread. Designed to work with unhandled errors. In a regular JVM, the error is written to the console by default. In Android, the application crashes. The place in the Android source code where this behavior is set:

https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/com/android/internal/os/RuntimeInit.java

CEH – coroutine exception handler. The essence of the coroutine context. Works similarly to UEH, but at the coroutine level, not at the thread level. Also serves to work with unhandled errors.

Throwing an error is the default behavior in the JVM, where an unhandled error moves up the thread's function stack until it is handled in a try-catch or UEH.

Propagate error is a behavior that comes from coroutines. When a coroutine encounters an unhandled error, the coroutine cancels itself and sends the error to its parent coroutine.

Initially, the goal was to understand how coroutines work in Android. For this reason, all the code presented below can be considered to be executed on Android and the main thread. But the rules will also work for JVM, only with the above-described difference in UEH behavior. I repeat that if it is written about the application crash, then this concerns the behavior on Android. On JVM, the thread will stop and the error will be displayed in the console.

Disclaimer

Anything in this material may be a lie. Do not make decisions based on this material. If you decide to make a decision, hire professionals or double-check the information yourself.

Based on the previous article (link), then in relation to coroutine we can say that on android it will cause an application crash if the UEH of the thread has not been redefined. Because launch will go with the exception to the parent (GlobalScope), and the parent does not know how to handle exceptions and will send it back to launch for processing. And launch, when handling an exception, sends an error to CEH, and if it is absent (as in the current example), it sends it to UEH.

But will this happen in all cases? Or is it possible to prevent this coroutine from closing the application with a crash without changing the coroutine and without changing the UEH of the thread?

To understand how this can be done, you need to go to the source code of coroutines.

  • Here – This is the place where it is determined who will handle the exception, the child or the parent coroutine.

  • Then here – StandaloneCoroutine is created when launch coroutine is created. It redefines the behavior to handle exceptions differently than in async.

  • More here – Here the standard logic for launch occurs, if there is CEH, then we send the error to it. Otherwise, we send it to UEH, which will happen in handleUncaughtCoroutineException.

  • And here – And this place will already help you find the answer to the questions asked above.

The handleUncaughtCoroutineException function can be divided into two parts. In the lower part, under the for loop, nothing interesting happens. We go to the UEH of the thread and send an exception there, as mentioned above. But then the question may arise, why do we need the upper part of handleUncaughtCoroutineException, where the for loop is, if all the work happens below? In the loop, the platformExceptionHandlers variable is passed, which is a collection of CEH to which exceptions are sent. Here another question may arise, where do CEH come from in platformExceptionHandlers? And if you go higher in the class, you can see that CEH in platformExceptionHandlers come from ServiceLoader.

Well, if one question has been answered, then the question of why logic with platformExceptionHandlers is needed remains.

To get at least one reason, you need to go to the AndroidExceptionPreHandler class. This class is an implementation of CEH, which is registered in META-INF and will be added to platformExceptionHandlers when connecting the library to the project via ServiceLoader.

Why do we need AndroidExceptionPreHandler?

In Android 8, the logic of working with exceptions has changed a little. Before Android 8, if you redefine UEH for a thread, the system did not add its logic in this case. Starting with Android 8 inclusive, if you redefine UEH for a thread, in addition to the redefined logic, the system also logs the exception. And for this reason, on Android 8, before sending an error to UEH, in coroutines, you need to call the Thread.getUncaughtExceptionPreHandler function, which is not in the original class from the JVM, so this must be done through reflection. In Android 9, this was fixed and the above fix through reflection is no longer needed.

https://developer.android.com/about/versions/oreo/android-8.0-changes#loue

And so it turns out that, in coroutines, before sending to UEH, the exception is first sent to the CEH collection obtained from ServiceLoader. If you go back to the for loop on the platformExceptionHandlers collection, you can see that if CEH throws an ExceptionSuccessfullyProcessed exception, then the handleUncaughtCoroutineException function will completely complete, and the exception will not get to UEH.

Thus, to avoid a crash when executing a coroutine from the beginning, you need to create a CEH instance and configure its META-INF by analogy with the AndroidExceptionPreHandler class. You also need CEH to throw ExceptionSuccessfullyProcessed.

For an example of how to create and configure such a CEH, you can see Here. An example of usage can be seen Here or include as dependency 'com.github.dracula6322:psychic-goggles:1.8.1.0' via jitpack for coroutines 1.8.1.

Why is java used in the examples?

Since ExceptionSuccessfullyProcessed has an internal modifier, it will be a bit difficult to call it from Kotlin. But from Java code it becomes a bit easier.

Conclusion: as a result, we got a way how to create a global CEH on coroutines, which will receive exceptions before they are processed in UEH. Since this is a setting for CEH, it will not affect async exceptions. At the moment, this method can hardly be called recommended, since at least ExceptionSuccessfullyProcessed has an internal modifier. Therefore, it is unlikely that you should base your business logic on this method. As an option, you can process (log or crash the application in testing) exceptions that were missed, so that you do not need to register CEH everywhere for all coroutines.

Similar Posts

Leave a Reply

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