Android exception in service

Error handling on Android part 1: how exceptions work for JVM and Android apps

Error handling on Android

How many times have you been in the middle of using a new shiny app, only to have it crash on you?

This is the first in a series of posts that will investigate how the exception handling mechanism works in Java and Android, and how crash reporting SDKs can capture diagnostic information, so that you’re not flying blind in production.

How do exceptions work for JVM and Android apps?

Exceptions should be thrown in exceptional circumstances where the calling code needs to decide how to recover from an error condition.

What is an Exception object?

A [CODE]Throwable[/CODE] is a special type of object which can be thrown and alter the execution path of a JVM application. For example, the code snippet below throws an [CODE]IllegalStateException[/CODE]:

— CODE language-kotlin —
fun main() <
В В В try <
В В В В В В В throw IllegalStateException()
В В В В В В В println(«Hello World»)
В В В > catch (exc: Throwable) <
В В В В В В В println(«Something went wrong»)
В В В >
>

Throwing an exception means that the execution flow changes, and ‘Hello World’ is never printed. Instead, the program counter will jump to the nearest catch block, and executes the error recovery code within, meaning our program prints ‘Something went wrong’ instead.

Of course, it doesn’t always make sense to recover from a failure — for example, if an [CODE]OutOfMemoryError[/CODE] is thrown by the JVM, there is very little prospect of ever recovering from this condition. In this case it makes sense to leave the [CODE]Throwable as unhandled[/CODE], and allow the process to terminate so the user can restart the app from a fresh state.

Anatomy of the Throwable class

[CODE]Throwable[/CODE] has two direct subclasses: [CODE]Exception[/CODE], and [CODE]Error[/CODE]. Typically an [CODE]Error[/CODE] is thrown in conditions where recovery is not possible, and an [CODE]Exception[/CODE] where recovery is possible. Additionally, there are many subclasses of [CODE]Exception[/CODE] which convey additional meaning — for example, an [CODE]IllegalArgumentException[/CODE] would indicate the programmer passed an invalid argument, and an [CODE]IllegalStateException[/CODE] would indicate that the program encountered an unanticipated state.

— CODE language-kotlin —
fun main() <
В В В try <
В В В В В В В throw IllegalStateException(«This should never happen!»)
В В В В В В В println(«Hello World»)
В В В > catch (exc: Throwable) <
В В В В В В В println(«Something went wrong»)
В В В >
>

Let’s consider the above snippet again. The constructed [CODE]IllegalStateException[/CODE] object captures a snapshot of the application at the time of the error condition:

— CODE language-kotlin —
java.lang.IllegalStateException: This should never happen!
at com.example.myapplication.Exceptions101Kt.foo(Exceptions101.kt:12)
at com.example.myapplication.Exceptions101Kt.main(Exceptions101.kt:5)
at com.example.myapplication.Exceptions101Kt.main(Exceptions101.kt)

This is commonly called a stacktrace. Each line represents a single frame in the application’s call stack at the time of the error, which match the filename, method name, and line number of our original code snippet.

Читайте также:  Управление андроидом удаленный экран

A stacktrace can also contain other useful information, such as program state, which in this case is a static error message, but we could equally pass in arbitrary variables.

Exception handling hierarchy

After throwing an exception, an exception handler must be found to handle the exception, or the app will terminate. In the JVM, this is a well-defined hierarchy, which we’ll run through here.

First up in the exception handling hierarchy is a catch block:

— CODE language-kotlin —
try <
В В В crashyCode()
> catch (exc: IllegalStateException) <
В В В // handle throwables of type IllegalStateException
>

If a catch block isn’t available in the current stack frame, but is defined further down the call stack, then the exception will be handled there.

Next in the hierarchy is implementations of [CODE]UncaughtExceptionHandler[/CODE]. This interface contains a single method which is invoked whenever a [CODE]Throwable[/CODE] is thrown, after the handler has been set:

— CODE language-kotlin —
val currentThread = Thread.currentThread()
currentThread.setUncaughtExceptionHandler < thread, exc ->
В В В // handle all uncaught JVM exceptions in the current Thread
>

It’s possible to set an [CODE]UncaughtExceptionHandler[/CODE] in a few different places; the JVM has a defined hierarchy for these. First, if a handler has been set on the current [CODE]Thread[/CODE], this will be invoked. Next up will be a handler on the [CODE]ThreadGroup[/CODE], before finally, the default handler is invoked, which will handle all uncaught JVM exceptions by printing a stacktrace, and then terminating the app.

— CODE language-kotlin —
Thread.setDefaultUncaughtExceptionHandler < thread, exc ->
В В В // handle all uncaught JVM exceptions
>

It’s the default [CODE]UncaughtExceptionHandler[/CODE] that is most interesting from an error reporting point-of-view, and it’s the default [CODE]UncaughtExceptionHandler[/CODE] that is responsible for showing that all too familiar crash dialog on Android.

The [CODE]UncaughtExceptionHandler[/CODE] interface is the building block of all crash reporting SDKs on the JVM, such as bugsnag-android or bugsnag-java. Read on in part two to learn how we can define a custom handler for uncaught exceptions, and use it to create a crash reporting SDK.

Would you like to know more?

Hopefully this has helped you learn a bit more about Error Handling on Android. If you have any questions or feedback, please feel free to get in touch.

Источник

Exceptions in coroutines

Cancellation and Exceptions in coroutines (Part 3) — Gotta catch ’em all!

We, developers, usually spend a lot of time polishing the happy path of our app. However, it’s equally important to provide a proper user experience whenever things don’t go as expected. On one hand, seeing an application crash is a bad experience for the user; on the other hand, showing the right message to the user when an action didn’t succeed is indispensable.

Handling exceptions properly has a huge impact on how users perceive your application. In this article, we’ll explain how exceptions are propagated in coroutines and how you can always be in control, including the different ways to handle them.

If you prefer video, check out this talk from KotlinConf’19 by Florina Muntenescu and I:

⚠️ In order to follow the rest of the article without any problems, reading and understanding Part 1 of the series is required.

Coroutines: First things first

Cancellation and Exceptions in Coroutines (Part 1)

A coroutine suddenly failed! What now? 😱

When a coroutine fails with an exception, it will propagate said exception up to its parent! Then, the parent will 1) cancel the rest of its children, 2) cancel itself and 3) propagate the exception up to its parent.

Читайте также:  Зачем обновлять версию андроида

The exception will reach the root of the hierarchy and all the coroutines that the CoroutineScope started will get cancelled too.

While propagating an exception can make sense in some cases, there are other cases when that’s undesirable. Imagine a UI-related CoroutineScope that processes user interactions. If a child coroutine throws an exception, the UI scope will be cancelled and the whole UI component will become unresponsive as a cancelled scope cannot start more coroutines.

What if you don’t want that behavior? Alternatively, you can use a different implementation of Job , namely SupervisorJob , in the CoroutineContext of the CoroutineScope that creates these coroutines.

SupervisorJob to the rescue

With a SupervisorJob , the failure of a child doesn’t affect other children. A SupervisorJob won’t cancel itself or the rest of its children. Moreover, SupervisorJob won’t propagate the exception either, and will let the child coroutine handle it.

You can create a CoroutineScope like this val uiScope = CoroutineScope(SupervisorJob()) to not propagate cancellation when a coroutine fails as this image depicts:

If the exception is not handled and the CoroutineContext doesn’t have a CoroutineExceptionHandler (as we’ll see later), it will reach the default thread’s ExceptionHandler . In the JVM, the exception will be logged to console; and in Android, it will make your app crash regardless of the Dispatcher this happens on.

💥 Uncaught exceptions will always be thrown regardless of the kind of Job you use

The same behavior applies to the scope builders coroutineScope and supervisorScope . These will create a sub-scope (with a Job or a SupervisorJob accordingly as a parent) with which you can logically group coroutines (e.g. if you want to do parallel computations or you want them to be or not be affected by each other).

Warning: A SupervisorJob only works as described when it’s part of a scope: either created using supervisorScope or CoroutineScope(SupervisorJob()) .

Job or SupervisorJob? 🤔

When should you use a Job or a SupervisorJob ? Use a SupervisorJob or supervisorScope when you don’t want a failure to cancel the parent and siblings.

In this case, if child#1 fails, neither scope nor child#2 will be cancelled.

In this case, as supervisorScope creates a sub-scope with a SupervisorJob , if child#1 fails, child#2 will not be cancelled. If instead you use a coroutineScope in the implementation, the failure will get propagated and will end up cancelling scope too.

Watch out quiz! Who’s my parent? 🎯

Given the following snippet of code, can you identify what kind of Job child#1 has as a parent?

child#1 ’s parentJob is of type Job ! Hope you got it right! Even though at first impression, you might’ve thought that it can be a SupervisorJob , it is not because a new coroutine always gets assigned a new Job() which in this case overrides the SupervisorJob . SupervisorJob is the parent of the coroutine created with scope.launch ; so literally, SupervisorJob does nothing in that code!

Therefore, if either child#1 or child#2 fails, the failure will reach scope and all work started by that scope will be cancelled.

Remember that a SupervisorJob only works as described when it’s part of a scope: either created using supervisorScope or CoroutineScope(SupervisorJob()) . Passing a SupervisorJob as a parameter of a coroutine builder will not have the desired effect you would’ve thought for cancellation.

Regarding exceptions, if any child throws an exception, that SupervisorJob won’t propagate the exception up in the hierarchy and will let its coroutine handle it.

Under the hood

If you’re curious about how Job works under the hood, check out the implementation of the functions childCancelled and notif y Cancelling in the JobSupport.kt file.

In the SupervisorJob implementation, the childCancelled method just returns false , meaning that it doesn’t propagate cancellation but it doesn’t handle the exception either.

Читайте также:  Файл xml android hardware usb host xml

Dealing with Exceptions 👩‍🚒

Coroutines use the regular Kotlin syntax for handling exceptions: try/catch or built-in helper functions like runCatching (which uses try/catch internally).

We said before that uncaught exceptions will always be thrown. However, different coroutines builders treat exceptions in different ways.

Launch

With launch, exceptions will be thrown as soon as they happen. Therefore, you can wrap the code that can throw exceptions inside a try/catch , like in this example:

With launch, exceptions will be thrown as soon as they happen

Async

When async is used as a root coroutine (coroutines that are a direct child of a CoroutineScope instance or supervisorScope ), exceptions are not thrown automatically, instead, they’re thrown when you call .await() .

To handle exceptions thrown in async whenever it’s a root coroutine, you can wrap the .await() call inside a try/catch :

In this case, notice that calling async will never throw the exception, that’s why it’s not necessary to wrap it as well. await will throw the exception that happened inside the async coroutine.

When async is used as a root coroutine, exceptions are thrown when you call .await

Also, notice that we’re using a supervisorScope to call async and await . As we said before, a SupervisorJob lets the coroutine handle the exception; as opposed to Job that will automatically propagate it up in the hierarchy so the catch block won’t be called:

Furthermore, exceptions that happen in coroutines created by other coroutines will always be propagated regardless of the coroutine builder. For example:

In this case, if async throws an exception, it will get thrown as soon as it happens because the coroutine that is the direct child of the scope is launch . The reason is that async (with a Job in its CoroutineContext ) will automatically propagate the exception up to its parent ( launch ) that will throw the exception.

⚠️ Exceptions thrown in a coroutineScope builder or in coroutines created by other coroutines won’t be caught in a try/catch!

In the SupervisorJob section, we mention the existence of CoroutineExceptionHandler . Let’s dive into it!

CoroutineExceptionHandler

The CoroutineExceptionHandler is an optional element of a CoroutineContext allowing you to handle uncaught exceptions.

Here’s how you can define a CoroutineExceptionHandler , whenever an exception is caught, you have information about the CoroutineContext where the exception happened and the exception itself:

Exceptions will be caught if these requirements are met:

  • When ⏰: The exception is thrown by a coroutine that automatically throws exceptions (works with launch , not with async ).
  • Where 🌍: If it’s in the CoroutineContext of a CoroutineScope or a root coroutine (direct child of CoroutineScope or a supervisorScope ).

Let’s see some examples using the CoroutineExceptionHandler defined above. In the following example, the exception will be caught by the handler:

In this other case in which the handler is installed in a inner coroutine, it won’t be caught:

The exception isn’t caught because the handler is not installed in the right CoroutineContext . The inner launch will propagate the exception up to the parent as soon as it happens, since the parent doesn’t know anything about the handler, the exception will be thrown.

Dealing with exceptions gracefully in your application is important to have a good user experience, even when things don’t go as expected.

Remember to use SupervisorJob when you want to avoid propagating cancellation when an exception happens, and Job otherwise.

Uncaught exceptions will be propagated, catch them to provide a great UX!

Источник

Оцените статью