Chapters

Hide chapters

Android Debugging by Tutorials

First Edition · Android 12 · Kotlin 1.6 · Android Studio Chipmunk (2021.2.1)

Section I: Debugging Basics

Section 1: 8 chapters
Show chapters Hide chapters

4. Analyzing the Stack Trace
Written by Zac Lippard

In the previous chapter, you learned about Logcat and how you can use the Logcat window to find bugs in your app. One common example of Logcat is finding stack traces for crashes and exceptions.

Analyzing a stack trace, and understanding how the trace itself is structured, provides clues as to why the app encountered the error, to begin with.

In this chapter, you’ll learn how to read a stack trace from the Podplay app, use tools to navigate through it and fix the associated bug behind the trace. You’ll be able to:

  • Read through a stack trace.
  • Catch errors and rethrow them with more information.
  • View the associated threads in a stack trace.
  • Add Firebase Crashlytics to your app.
  • Import a Crashlytics stack trace and fix the underlying error.

Defining Stack Trace

A stack trace shows a list of methods called at a certain place in your code. This list is typically referred to as the stack frame or call stack. In Android development, an exception generates a stack trace. You can review the stack trace to determine the underlying cause of a bug in your app.

For example, you may have the following methods, a(), b() and c():

fun a() {
  b()
}

fun b() {
  c()
}

fun c() {
  throw Exception("Uh oh!")
}

When the exception throws, the stack trace will contain the methods with the most recent method call at the top:

c()
b()
a()

Stack traces in Android will also include line numbers to denote where the next function call is in the stack.

Next, you’ll learn how to read and utilize stack traces to fix a bug in the Podplay app.

Reading a Stack Trace

Open the Podplay starter project and run the app. After the app launches, tap the search icon, type in “sermon audio” and press Return.

Once the list of podcasts appears, tap the first podcast in the list. The app crashes!

Open the Logcat window in Android Studio, switch to the Error type, and find a stack trace similar to the following:

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.yourcompany.podplay, PID: 22519
    java.text.ParseException: Unparseable date: "Fri, 11 Feb 2022 02:10 GMT"
        at java.text.DateFormat.parse(DateFormat.java:362)
        at com.yourcompany.podplay.util.DateUtils.xmlDateToDate(DateUtils.kt:56)
        at com.yourcompany.podplay.repository.PodcastRepo.rssItemsToEpisodes(PodcastRepo.kt:75)
        at com.yourcompany.podplay.repository.PodcastRepo.rssResponseToPodcast(PodcastRepo.kt:88)
        at com.yourcompany.podplay.repository.PodcastRepo.getPodcast(PodcastRepo.kt:61)
        at com.yourcompany.podplay.repository.PodcastRepo$getPodcast$1.invokeSuspend(Unknown Source:15)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

There are a few things to explain:

  1. Look at the first line:

    E/AndroidRuntime: FATAL EXCEPTION: main
    

    The E/AndroidRuntime part denotes that this was an error log with AndroidRuntime as the tag. You’ll also see that the exception occurred on the main thread.

  2. Notice the second line:

    Process: com.yourcompany.podplay, PID: 22519
    

    It provides process-specific information, including the process name, the app’s package ID and the process ID.

  3. Move to the next line:

    java.text.ParseException: Unparseable date: "Fri, 11 Feb 2022 02:10 GMT"
    

    The third line details what type of exception was thrown, along with an associated message attached to the exception. In the case above, the ParseException, was thrown because the date provided isn’t parsable.

  4. The remaining lines detail the call stack. Each following line represents a method, and the associated line number is provided. The line numbers from the previous method in the stack correlate to the method call above it. For example:

    at java.text.DateFormat.parse(DateFormat.java:362)
    at com.yourcompany.podplay.util.DateUtils.xmlDateToDate(DateUtils.kt:56)
    

These lines represent that at line 56 of the DateUtils class xmlDateToDate() makes a call to inFormat.parse(date) ?: Date().

Knowing where the crash occurs in the code is crucial to fixing the underlying problem. Now that you know what is causing the crash, it’s time to fix it!

Catching and Rethrowing Errors

In many cases, the best way to prevent crashes from thrown exceptions is to catch the exception in a try/catch block. The try block will run your code, and the catch block will catch the exception types you specify. You can use this to handle the crash above.

Note: Catching exceptions isn’t always necessary. It may make more sense to fix the crash in certain situations. For example, if you encounter a NullPointerException you may need to add a null check to your code to prevent the offending line of code from being reached if a null value is provided.

Open DateUtils.kt and find xmlDateToDate(). Remember, based on the stack trace line 56 is the culprit:

return inFormat.parse(date) ?: Date()

On that line, hover your cursor over parse(), and the code documentation for that method appears. If the code documentation isn’t displayed, move your cursor to the method and press F1. The code documentation explains that the method will throw a ParseException when the date formatter can’t correctly parse the provided date text.

Now that you know parse() can throw an exception, it’s time to handle it. Replace line 56 by wrapping the logic inside a try/catch block:

return try {
  inFormat.parse(date) ?: Date()
} catch (e: ParseException) {
  Log.wtf("xmlDateToDate", e)
  Date()
}

Note: Make sure you use thejava.text.ParseException import

The return statement takes the entire try/catch block. If you can parse the date string, or if parse() returns null, a Date object will be returned in the try block.

But, if parse() throws a ParseException, your code will now catch this. First, the exception will be logged using the Log.wtf() (which stands for “What a Terrible Failure” of course!) providing the custom error tag of “xmlDateToDate” and the exception e. Then, you set a new Date instance as a return value. Providing a Date object even in the catch block ensures that the app can continue moving forward.

Rerun the app and follow the steps noted earlier to try to reproduce the crash. This time there’s no crash!

Open the Logcat window in Android Studio, and you’ll find the exception:

2022-05-18 22:37:44.092 22747-22747/com.yourcompany.podplay E/xmlDateToDate: Unparseable date: "Thu, 10 Feb 2022 01:25 GMT"
    java.text.ParseException: Unparseable date: "Thu, 10 Feb 2022 01:25 GMT"
        at java.text.DateFormat.parse(DateFormat.java:362)
        at com.yourcompany.podplay.util.DateUtils.xmlDateToDate(DateUtils.kt:59)
        at com.yourcompany.podplay.repository.PodcastRepo.rssItemsToEpisodes(PodcastRepo.kt:75)
        at com.yourcompany.podplay.repository.PodcastRepo.rssResponseToPodcast(PodcastRepo.kt:88)
        at com.yourcompany.podplay.repository.PodcastRepo.getPodcast(PodcastRepo.kt:61)
        at com.yourcompany.podplay.repository.PodcastRepo$getPodcast$1.invokeSuspend(Unknown Source:15)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

Notice that in the first line the tag now shows up as E/xmlDateToDate rather than E/AndroidRuntime. The stack trace still exists for reference, but the app successfully recovered from the thrown exception.

Great work on fixing this bug! :]

Wouldn’t it be great, though, if crash data from real users came to you rather than manually searching through the code or trying to reproduce crashes in order to get the stack trace? Well, there’s a way to do that. Next, you’ll learn how Firebase Crashlytics can provide some automation around tracking crashes.

Firebase Crashlytics

Crashlytics is an invaluable tool that provides you with crash reports, non-fatal errors, and Application Not Responding (ANR) errors. Firebase is Google’s suggested app development platform which hosts Crashlytics as a service, along with several other great tools.

Next, you’ll learn how to set up a Firebase account and connect it to the Podplay project. You’ll then use Crashlytics to view crashes that occur in Podplay.

Creating a Firebase Project

To start, go to the Firebase Console and set up a new Firebase project. Click Add project.

Next, enter the project name. It can be Podplay, similar to the app, or anything that’ll help you correlate the Firebase project to your app. Check the confirmation checkbox and select Continue.

The next step will ask you to enable Google Analytics for the Firebase project. Crashlytics will use Google Analytics to determine crash-free user data. Make sure the toggle is on, and move forward using Continue.

In the last step, choose the Google Analytics account to which you want the Firebase Project linked. Then click Create project.

The project gets created, and you should see a loading indicator while the project finalization wraps up.

When the project is ready, select Continue.

You’ll redirect to the new Firebase project’s dashboard. Now it’s time to add your app to the Firebase project.

Adding Podplay to the Firebase Project

You can add the Podplay app to the Firebase project from the dashboard. Click the Android icon to start the process of adding Podplay.

The first step is to register your app. Enter in the package name of the Podplay app: com.yourcompany.podplay

You may also enter in an App’s nickname if you’d like.

Also optional is the debug signing certificate SHA-1. This is useful for securing the Firebase project by only allowing app builds signed with the debug key on your workstation.

To get the SHA-1 of the debug certificate, open a Terminal window and navigate to the home directory on your workstation. Next, run the following command:

keytool -list -v \
-alias androiddebugkey -keystore .android/debug.keystore

If prompted for a password, enter android. Then, keytool will print out the certificate fingerprints for the androiddebugkey alias. Copy the SHA-1 fingerprint and paste it in to the form field.

Next, click Register app. It’ll prompt you to download the google-services.json file and add it to the app/ folder. This file provides all the configuration data the app can use to communicate with the Firebase project.

Select Download google-services.json. After downloading, copy the file to the Podplay project’s app/ directory. In Android Studio, change to the Project view in the Project window, and you’ll now see google-services.json in the directory.

Back on the Firebase console’s app registration page, select Next. You’ll need to update your project-level and app-level build.gradle to include the Firebase dependencies.

Open the project-level build.gradle first, and add the following class paths to the dependencies list:

classpath 'com.google.gms:google-services:4.3.10'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.0'

Note: The Firebase setup also mentions the need to add the google() maven repository, but these have already been added to build.gradle.

Now, open the app-level app/build.gradle. Add the following plugins below the list of other applied plugins:

apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'

Add the imports for the Firebase BoM, bill of materials platform, and specify the usage of the Firebase Crashlytics and Analytics libraries:

implementation platform('com.google.firebase:firebase-bom:30.0.1')
implementation 'com.google.firebase:firebase-crashlytics-ktx'
implementation 'com.google.firebase:firebase-analytics-ktx'

Note: With the use of the Firebase BoM platform, you don’t need to specify the versions on the individual libraries as the platform is aware of what versions to use.

Click the Sync project with gradle files icon in the toolbar to pull down the Firebase SDK into the project.

Once the gradle sync finishes, rerun the Podplay app.

Now, switch back to the Firebase web page and click Next. You’re all done with the setup! Choose Continue to console to return to the Podplay Firebase project’s dashboard.

Previewing Crashes Within Crashlytics

From the Firebase project dashboard, select the Crashlytics option under Release & Monitor on the left-hand column.

You’ll see that the Crashlytics page is waiting for the app’s first crash.

Now you need to crash the app. In Android Studio, open PodcastActivity.kt and add the following line in onCreate() right after the super.onCreate() call:

throw RuntimeException("Test for Crashlytics!")

Run the app and go back to the Firebase project web page. You’ll see that it has detected the crash and that the installation is complete! Click Go to Crashlytics dashboard.

On the Crashlytics page, there’s now one crash recorded:

Scrolling down a bit, you’ll see the PodcastActivity.kt crash listed as a “fresh issue”.

Click the issue to view the details page. Scroll down to the Event summary section to find the Stack trace tab with information on the stack trace itself.

You’ll notice the RuntimeException with the test message. Click the down arrow to expand the stack trace fully.

The Data tab also provides more information about the crash, the device it occurred on and the OS running on it. This can be useful for pinpointing bugs specific to certain devices or operating systems or determining if there are any potential memory leaks.

Great job on setting up Firebase Crashlytics! Now that you’ve got the crashes reporting to the Crashlytics service, it’s time to try importing Crashlytics stack traces into Android Studio to inspect them.

Analyzing External Stack Traces

Now that you’ve got a stack trace from Crashlytics, it’s time to incorporate that with Android Studio to fix the bug you created!

In the Stack trace tab, click the TXT tab to switch to the plain text view of the stack trace. Copy the stack trace in its entirety.

In Android Studio, navigate through toolbar options Code ▸ Analyze Stack Trace or Thread Dump. In the dialog window, paste the stack trace from Crashlytics and click OK.

A new tab in the Run window will appear with the stack trace provided. Text formatting will apply and the section PodcastActivity.kt:84 will highlight as a link.

Click the link, and it’ll take you to the line with your throw RuntimeException() call.

Delete that throw call, and you’ve fixed the app.

Congrats! You’ve now mastered the art of reading and analyzing stack traces.

Challenge

It’s time to put what you’ve learned to use. There’s another bug that is present in Podplay. Launch the app and search for any podcast. When the list of podcasts displays, repeatedly tap one until the app crashes.

Use the skills you’ve learned in this chapter to find the stack trace and the underlying bug, and fix it.

Key Points

  • A stack trace shows a list of methods called at a certain place in your code.
  • Understand the method calls that led up to the crash by reading a stack trace.
  • You can capture crashes and rethrow them as non-fatal errors.
  • Use Firebase console to connect Crashlytics to any Android app.
  • You can utilize Crashlytics to monitor crashes and find stack traces.

Where to Go From Here?

Firebase Crashlytics is a powerful tool that you have at your disposal. You can find more in-depth details in the Firebase Crashlytics documentation about the Crashlytics service and what you can do with it.

In the next chapter, you’ll learn how to manipulate data at runtime while debugging your app. This will allow you to simulate certain situations and put your app in a state that replicates crashes that your users will experience.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.