Better App Experiences Workshop: Join us Thursday, April 11th and improve mobile performance by removing friction and frustration.

Register today!
Crash Reporting

Solving “expired task” crashes

Solving an expired task crash in iOS is vital for ensuring an optimal mobile user experience. Find out how you can pinpoint and solve these crashes in our blog post.

Editor’s Note: This blog was originally published on Mar. 4, 2021 and republished May 17, 2023 to ensure all content and links are up to date and accurate.

iOS has always had the concept of the “background user task system”: this is how the phone’s operating system grants applications time to run in the background and finish what they were doing. For example, if you were to background a messaging app while it was still processing a message the app can only finish the network calls if the background user task system gives it permission to continue running. Unfortunately, in some cases, this process can cause the app to crash and ruin the user’s experience. As an engineer, you must deliver high-quality experiences to retain users in such a competitive market. We’ll discuss how to solve this problem below. In this post, we’ll cover the following:

  • What causes expired task crashes?
  • Why is solving expired task crashes important?
  • How can engineers solve expired task crashes?

What causes expired task crashes?

First, what happens when an app is forced to suspend and is moved into the background? If the user presses the home button and the app has no running tasks to complete, it suspends immediately. This is the “default” on iOS — immediate background suspension.

Normally, engineers prevent this through requesting extra background time by explicitly calling the beginBackgroundTaskWithExpirationHandler or its variants. Doing so grants your app 30 seconds under normal power conditions to finalize any work before the app will be forced to suspend.

This changes the result, and when the app is forced to suspend, the system allocates expiration handlers to expiring modules that gives them permission to finish up their work. After the 30 second period, all the expiration handlers are called back by the system, and the app is given one additional second to finish up any remaining work. If any tasks are still running after this grace period, the application is terminated by the system instead of suspended, resulting in an expired task crash.

Why is solving expired task crashes important?

Although these crashes occur in the background, your user isn’t entirely unaware. Expired task crashes wipe the cache of whatever the user was doing in that app (such as filling out a form) before the app was terminated and can cause data to be lost. When this happens, the next time your user launches the app they are back on the app’s starting screen and not where they left off.

For many apps, this isn’t just a bad user experience but can translate to a loss in sales. For example, imagine if your user was in the middle of adding items to their shopping cart in your app, then suspended your app to respond to a text message. They switch back and the app has crashed, needing them to refill their shopping cart again. But this time around they can’t remember what they had previously put in, resulting in them buying only the essentials and causing you to lose out on sales. Or they might no longer trust your app with their credit card information and instead choose to uninstall it.

In the worst case, these crashes can also lead to data corruption. If your task is terminated in the middle of writing data to the disk, that data may now be corrupt and lead to further crashes on subsequent launches. This corruption can build up in your app on user devices without your knowledge, causing the user experience to deteriorate over time.

All of these issues happen because of expired task crashes that could have been caught ahead of time and resolved.

How can we solve expired task crashes?

Usually, you don’t even know about them — these expired task crashes generate iOS crash reports that are considered private by Apple and are not usually given to engineers. Even if you are aware of the issue, the problem is still difficult to track because Apple hides the name of the task or module from you in the iOS crash logs. With the right tooling, you can diagnose (and subsequently solve) an expired task crash using the following code:

// example background task to run

        if #available(iOS 13.0, *) {

            BGTaskScheduler.shared.register(forTaskWithIdentifier: 
            /* be sure to have an ID specific to the task and not re use IDs */"com.example.background", 
            using: DispatchQueue.global()) { task in

                task.expirationHandler = {

                    // This happens when the background task is about to run out of time.

                    // This adds a breadcrumb to the session timeline. 
                    You can use this to get an idea of which specific background task is failing.

                    // This is absolutely fine (and even encouraged) to be used in production.

                    Embrace.sharedInstance().logBreadcrumb(withMessage: "background task (task.identifier) is about to expire !")

                    // this adds a log message to the "log" section of the embrace dash. 
                    You can use this to filter for and add more information about tasks 
                    that are about to expire.                    

                    // this requires a network call to be made so removing this specific line for production 
                    is probably a good idea as it might cause the task to expire on its own, 
                    however, it can be used for test flight or QA builds to help diagnose the problem in non production settings   

                    Embrace.sharedInstance().logMessage("background task is about to expire !", with: .warning, properties:

                        /* This is to be able to filter in the dash for specific background IDs that are about to expire */

                        ["background_identifier":task.identifier])

                }                

            // do something in background

            }

            // example request background task            

            do {

                // Request specific background task ID

                let processingTaskRequest = BGProcessingTaskRequest(identifier: "com.example.background")

                // ensure network ability for embrace to send the "late" message

                // this is only required if Embrace.sharedInstance().logMessage is used. 
                It is not necessary for Embrace.sharedInstance().logBreadcrumb

                processingTaskRequest.requiresNetworkConnectivity = true

                try BGTaskScheduler.shared.submit(processingTaskRequest)

            } catch {

                print(error.localizedDescription)

            }

        }

Although you may be familiar with swizzling, we don’t recommend this approach because it’s error-prone, known to introduce unexpected behavior, and can negatively impact app performance. A good rule of thumb is: If swizzling isn’t your only option, then it should be your last option.

Key takeaways

Solving expired task crashes in iOS is crucial for ensuring a seamless mobile user experience. Embrace’s dedication to addressing these issues not only improves app stability and user satisfaction but also saves valuable engineer time. With our commitment to helping mobile engineers build better experiences, we can help ensure that users have the best possible experience with your iOS app.

Get to the bottom of your expired task crashes quickly, with Embrace.

Embrace Deliver incredible mobile experiences with Embrace.

Get started today with 1 million free user sessions.

Get started free

Build better mobile apps with Embrace

Find out how Embrace helps engineers identify, prioritize, and resolve app issues with ease.