Back to Blog Home

How to handle Android exceptions and avoid application crashes

Ruben Quadros image

Ruben Quadros -

How to handle Android exceptions and avoid application crashes

Let's start by stating the obvious: an exception is a problem that occurs during the runtime of a program which disrupts its conventional flow and exception handling is the process of responding to an exception.

In Android, not handling an exception will lead to your application crashing and you seeing the dreaded "App keeps stopping" dialog. This makes handling exceptions incredibly important, and let’s face it: no one is going to use an app that continually crashes.

In this article I’ll show you a few different ways to handle exceptions, as well as how to deal with unhandled exceptions. I’ll also show you how to capture these exceptions. Since we are focusing on Android, all the examples will be in Kotlin.

Exception hierarchy

All exceptions are subclasses of Throwable. According to the docs:

The Throwable class is the superclass of all errors and exceptions in the Java language. Only objects that are instances of this class (or one of its subclasses) are thrown by the Java Virtual Machine or can be thrown by the Java throw statement.

Types of exceptions

In Java, exceptions are mainly categorized into 2 types: checked and unchecked exceptions.

Checked exceptions: These are exceptions which are checked at compile time. If you call a method which throws an exception, then as the caller you should either handle this exception or declare it in the throws clause.
Examples of checked exceptions: IOException, NoSuchMethodException, ClassNotFoundException

Unchecked exceptions: These are exceptions which are not checked at compile time.

Examples of unchecked exceptions: NullPointerException, ArithmeticException, ArrayIndexOutOfBoundsException

One interesting thing to note is Kotlin doesn’t have checked exceptions.
Consider the following code:

fun doSomething() { val list = listOf(“one”, “two”, “three”) val item = list[4] println(“Item: $item”) }

What do you think would happen if you call this method? If your answer was that it will throw a runtime exception - you are correct.

Let’s consider another piece of code:

fun getString() = “Hello!” fun doSomething() { val result = getString() as Int println(“Result: $result”) }

If doSomething method is called, you will get a ClassCastException.

If you want to run these snippets you can do so here.

Sometimes, the built in exception classes are not enough to define a certain scenario. In this case you can even create your own exceptions.

class MyException( override val message: String = “Something went wrong!” ) : Exception(message = message) //you can throw it as follows fun doSomething() { throw MyException(message = “Error”) }

Different ways of exception handling

Now that you know about exceptions, let’s see how you can handle them. Let’s consider a small piece of code which throws an exception.

fun throwException() { throw Exception(“Manual Exception”) }

If we call this method and do not handle the exception, our application will crash and you’ll end up with the "App keeps stopping" dialog. The easiest way to handle this exception is to catch it and wrap the method call in a try block. You can also do some cleaning up in the finally block if required.

try { throwException() } catch (e: Exception) { //do something //once this exception is handled } finally { //do some clean up }

If you just want to catch some exception and do not have any additional work to execute in the finally block, Kotlin provides a runCatching function.

runCatching { throwException() }

If you want to run these snippets you can do so here.

Propagating exceptions

There could be certain scenarios where a method throws different exceptions and you have to catch only some of them and propagate the others higher up. Consider the following piece of code.

fun throwDifferentExceptions() { if (condition1) { throw IllegalArgumentException() } if (condition2) { throw CustomException1() } if (condition3) { throw CustomException2() } }

Using try/catch you can do the following:

try { throwDifferentExceptions() } catch (e: IllegalArgumentException) { //do something if required //and also propagate this higher up throw e } catch (e: CustomException1) { //handle this exception here //do not propagate this } catch (e: CustomException2) { //handle this exception here //do not propagate this }

Using runCatching you can do the following:

runCatching { throwDifferentExceptions() }.onFailure { e: Throwable -> when (e) { Is IllegalArgumentException -> { //handle this exception here //do not propagate this } Is CustomException1 -> { //do something if required //and also propagate this higher up throw e } Is CustomException2 -> { //handle this exception here //do not propagate this } else -> { //handle any unknown exception here //do not propagate this } } }

Important thing to note here is that since you are propagating the exceptions you will have to handle them higher up.

If you want to run these snippets you know the drill.

Android exception handling

Let’s consider a sample application which uses the above techniques to handle exceptions. The app has a single screen with two buttons which simulate doing some work when they are clicked. The first button has the necessary exception handling and clicking on the second button will cause the app to crash.

The code is quite simple:

@Composable fun HomeScreen(handleException: () -> Unit, unhandledException: () -> Unit) { Column(modifier = Modifier.fillMaxSize()) { Button( modifier = Modifier.align(Alignment.CenterHorizontally), onClick = handleException ) { Text(text = "Handle exception") } Button( modifier = Modifier.align(Alignment.CenterHorizontally), onClick = unhandledException ) { Text(text = "Unhandled exception") } } } class HomeViewModel : ViewModel() { fun handleException() { viewModelScope.launch { runCatching { doWork() } } } fun unhandledException() { viewModelScope.launch { doWork() } } private fun doWork() { throw CustomException() } }

In a real world application it is incredibly difficult to handle all exceptions even if we try our best. Maybe we get an internal library exception or maybe there was an edge case scenario which went unhandled. What steps can we take to minimize how often our application crashes?

It's possible to fallback to a default exception handler using the Thread.UncaughtExceptionHandler interface. However, I would never recommend doing this in your debug app as you would always want immediate feedback for any crash.

////////////////////////////////////////////// // Activity code ////////////////////////////////////////////// val exceptionHandler = Thread.UncaughtExceptionHandler { _: Thread, e: Throwable -> handleUncaughtException(e) } private fun attachUnhandledExceptionHandler() { if (BuildConfig.DEBUG.not()) { Thread.setDefaultUncaughtExceptionHandler(exceptionHandler) } } private fun handleUncaughtException(e: Throwable) { // do something for this exception }

Now when we click the second button, even though we have not explicitly handled the exception, the app doesn't crash.

So far we have seen different ways to handle exceptions as well as how to provide a default unhandled exception handler. Now, let’s go one step further and see how we can monitor these exceptions.

Reporting exceptions

Once you have released your app, you will want to track all errors/exceptions that occur. Sentry's Android SDK provides a way to do that and much more. You can first create an account, then set up a project to monitor your application. You can follow these steps to add the Android SDK to your android application.

Once setup is complete, let’s go ahead and start reporting the exceptions. To capture an exception you can call:

Sentry.captureException(throwable)

As you probably already guessed, you can do this in your catch block or in the onFailure block to record all the exceptions. Let’s go ahead and add this line to our handleUncaughtException method. You will be able to see all the captured details in your Sentry dashboard.

By clicking on an issue you can see the detailed stacktrace:

You also get the breadcrumbs, which provide a trail of events that happened prior to an exception.

Sentry goes beyond error handling with a suite of capabilities like Performance Monitoring and Profiling. We'll save those for a future post.

A quick recap:

  • An exception is a problem that occurs during the runtime of a program which disrupts its conventional flow

  • All exceptions are subclasses of Throwable

  • There are generally 2 types of exceptions: checked and unchecked

  • You can handle exceptions using: try/catch or runCatching

  • In Android, you can handle all unhandled exceptions using: Thread.UncaughtExceptionHandler

  • You can report exceptions via Sentry's Android SDK using: Sentry.captureException(throwable)

I hope now you have a better understanding of different exceptions as well as how you can handle and report them.

You can find the complete code for the sample application in this repository:
https://github.com/rubenquadros/Sentry-Exception-Handling

Share

Share on Twitter
Share on Bluesky
Share on HackerNews
Share on LinkedIn

Published

Sentry Sign Up CTA

Code breaks, fix it faster

Sign up for Sentry and monitor your application in minutes.

Try Sentry Free

Topics

Guest Posts

New product releases and exclusive demos

Listen to the Syntax Podcast

Of course we sponsor a developer podcast. Check it out on your favorite listening platform.

Listen To Syntax
© 2024 • Sentry is a registered Trademark of Functional Software, Inc.