What are the exceptions in android

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.

Источник

Coroutines: first things first

Cancellation and Exceptions in Coroutines (Part 1)

This series of blog posts goes in-depth into cancellation and exceptions in Coroutines. Cancellation is important for avoiding doing more work than needed which can waste memory and battery life; proper exception handling is key to a great user experience. As the foundation for the other 2 parts of the series (part 2: cancellation, part 3: exceptions), it’s important to define some core coroutine concepts such as CoroutineScope , Job and CoroutineContext so that we all are on the same page.

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

CoroutineScope

A CoroutineScope keeps track of any coroutine you create using launch or async (these are extension functions on CoroutineScope ). The ongoing work (running coroutines) can be canceled by calling scope.cancel() at any point in time.

You should create a CoroutineScope whenever you want to start and control the lifecycle of coroutines in a particular layer of your app. In some platforms like Android, there are KTX libraries that already provide a CoroutineScope in certain lifecycle classes such as viewModelScope and lifecycleScope .

When creating a CoroutineScope it takes a CoroutineContext as a parameter to its constructor. You can create a new scope & coroutine with the following code:

A Job is a handle to a coroutine. For every coroutine that you create (by launch or async ), it returns a Job instance that uniquely identifies the coroutine and manages its lifecycle. As we saw above, you can also pass a Job to a CoroutineScope to keep a handle on its lifecycle.

CoroutineContext

The CoroutineContext is a set of elements that define the behavior of a coroutine. It’s made of:

  • Job — controls the lifecycle of the coroutine.
  • CoroutineDispatcher — dispatches work to the appropriate thread.
  • CoroutineName — name of the coroutine, useful for debugging.
  • CoroutineExceptionHandler — handles uncaught exceptions, will be covered in Part 3 of the series.

What’s the CoroutineContext of a new coroutine? We already know that a new instance of Job will be created, allowing us to control its lifecycle. The rest of the elements will be inherited from the CoroutineContext of its parent (either another coroutine or the CoroutineScope where it was created).

Since a CoroutineScope can create coroutines and you can create more coroutines inside a coroutine, an implicit task hierarchy is created. In the following code snippet, apart from creating a new coroutine using the CoroutineScope , see how you can create more coroutines inside a coroutine:

The root of that hierarchy is usually the CoroutineScope . We could visualise that hierarchy as follows:

Job lifecycle

A Job can go through a set of states: New, Active, Completing, Completed, Cancelling and Cancelled. While we don’t have access to the states themselves, we can access properties of a Job: isActive , isCancelled and isCompleted .

If the coroutine is in an active state, the failure of the coroutine or calling job.cancel() will move the job in the Cancelling state ( isActive = false , isCancelled = true ). Once all children have completed their work the coroutine will go in the Cancelled state and isCompleted = true .

Parent CoroutineContext explained

In the task hierarchy, each coroutine has a parent that can be either a CoroutineScope or another coroutine. However, the resulting parent CoroutineContext of a coroutine can be different from the CoroutineContext of the parent since it’s calculated based on this formula:

Parent context = Defaults + inherited CoroutineContext + arguments

  • Some elements have default values: Dispatchers.Default is the default of CoroutineDispatcher and “coroutine” the default of CoroutineName .
  • The inherited CoroutineContext is the CoroutineContext of the CoroutineScope or coroutine that created it.
  • Arguments passed in the coroutine builder will take precedence over those elements in the inherited context.

Note: CoroutineContext s can be combined using the + operator. As the CoroutineContext is a set of elements, a new CoroutineContext will be created with the elements on the right side of the plus overriding those on the left. E.g. (Dispatchers.Main, “name”) + (Dispatchers.IO) = (Dispatchers.IO, “name”)

Читайте также:  Tablet with windows and android

Now that we know what’s the parent CoroutineContext of a new coroutine, its actual CoroutineContext will be:

New coroutine context = parent CoroutineContext + Job()

If with the CoroutineScope shown in the image above we create a new coroutine like this:

What’s the parent CoroutineContext of that coroutine and its actual CoroutineContext ? See the solution in the image below!

The resulting parent CoroutineContext has Dispatchers.IO instead of the scope’s CoroutineDispatcher since it was overridden by the argument of the coroutine builder. Also, check that the Job in the parent CoroutineContext is the instance of the scope’s Job (red color), and a new instance of Job (green color) has been assigned to the actual CoroutineContext of the new coroutine.

As we will see in Part 3 of the series, a CoroutineScope can have a different implementation of Job called SupervisorJob in its CoroutineContext that changes how the CoroutineScope deals with exceptions. Therefore, a new coroutine created with that scope can have SupervisorJob as a parent Job . However, when the parent of a coroutine is another coroutine, the parent Job will always be of type Job .

Now that you know the basics of coroutines, start learning more about cancellation and exceptions in coroutines with Part II and Part III of this series:

Источник

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.

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!

Источник

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