Thanks for being a part of WWDC25!

How did we do? We’d love to know your thoughts on this year’s conference. Take the survey here

BGTaskScheduler crashes on iOS 18.4

I've been seeing a high number of BGTaskScheduler related crashes, all of them coming from iOS 18.4. I've encountered this myself once on launch upon installing my app, but haven't been able to reproduce it since, even after doing multiple relaunches and reinstalls. Crash report attached at the bottom of this post.

I am not even able to symbolicate the reports despite having the archive on my MacBook:

Does anyone know if this is an iOS 18.4 bug or am I doing something wrong when scheduling the task? Below is my code for scheduling the background task on the view that appears when my app launches:

.onChange(of: scenePhase) { newPhase in
    if newPhase == .active {
        #if !os(macOS)
        let request = BGAppRefreshTaskRequest(identifier: "notifications")
        request.earliestBeginDate = Calendar.current.date(byAdding: .hour, value: 3, to: Date())
        do {
            try BGTaskScheduler.shared.submit(request)
            Logger.notifications.log("Background task scheduled. Earliest begin date: \(request.earliestBeginDate?.description ?? "nil", privacy: .public)")
        } catch let error {
            // print("Scheduling Error \(error.localizedDescription)")
            Logger.notifications.error("Error scheduling background task: \(error.localizedDescription, privacy: .public)")
        }
        #endif
...
}

Answered by DTS Engineer in 826562022

Submitted a bug report: FB16595418

Looking the data over, I think this is bug on our side, as the crash is actually coming from SwiftUI's background task integration, not your own code. It's possible there is a timing issue between your usage and SwiftUI, but that would still mean that SwiftUI changed "something" that altered the timing of activity.

When do you call "BGTaskScheduler.register(forTaskWithIdentifier:using:launchHandler:)"? That's the one behavior you have control over which could be a factor in this crash.

Having said all that, please replicate the issue a few time, collect a sysdiagnose, upload it to your bug, and then let me know here when all of that is done. The sysdiagnose should clarify exactly what's going wrong.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Our engineering teams need to investigate this issue, as this might indicate an issue with iOS 18.4 which is still in Beta.

We'd greatly appreciate it if you could open a bug report, include crash logs and sample code or models that reproduce the issue, and post the FB number here once you do.

Bug Reporting: How and Why? has tips on creating a successful bug report.

Same here. Happens only on iOS 18.4

Submitted a bug report: FB16595418

Same. Submitted logs in FB16610879

Submitted a bug report: FB16595418

Looking the data over, I think this is bug on our side, as the crash is actually coming from SwiftUI's background task integration, not your own code. It's possible there is a timing issue between your usage and SwiftUI, but that would still mean that SwiftUI changed "something" that altered the timing of activity.

When do you call "BGTaskScheduler.register(forTaskWithIdentifier:using:launchHandler:)"? That's the one behavior you have control over which could be a factor in this crash.

Having said all that, please replicate the issue a few time, collect a sysdiagnose, upload it to your bug, and then let me know here when all of that is done. The sysdiagnose should clarify exactly what's going wrong.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hi Kevin,

There's no usage of BGTaskScheduler.register anywhere in my app. Am I supposed to call this method at some point?

I use the background tasks API in two places currently:

  1. Calling BGTaskScheduler.shared.submit during .onChange(of: scenePhase) when newPhase == .active, on the view that appears when my app is launched. It schedules the background task whenever my app is brought to the foreground.

  2. .backgroundTask(.appRefresh("notifications")) attached to my WindowGroup. I attached the code snippet of this in my initial bug report.

I have just managed to reproduce the crash after quitting and relaunching the app about 100 times. The bug report has been updated with the sysdiagnose and a copy of the crash report.

Also happening here. I use .backgroundTask modifier on WindowGroup. Only an issue with 18.4.

There's no usage of BGTaskScheduler.register anywhere in my app. Am I supposed to call this method at some point?

Yes, very much so. Looking thing over a bit more closely, I think your app may basically have been working by "accident". What you're supposed to do is:

Failing to do so SHOULD crash your app with exactly this crash:

Fatal Exception: NSInternallnconsistencyException
All launch handlers must be registered before application finishes launching

With all that background, there are two more points:

1) Why you didn't crash before

My guess is that the generic "notifications" ID was actually being registered and used by something else in the system.

 let request = BGAppRefreshTaskRequest(identifier: "notifications")
 

Using an short ID like that was a mistake in our part and correcting that mistake would have created the crash you're currently seeing.

2) Your code isn't actually doing anything (sort of).

All this call does:

try BGTaskScheduler.shared.submit(request)

...is tell the system "I'd like to run this work at some point in the future, so let me know when I should run". The way that work is actually supposed to run is through the block you setup in "register(forTaskWithIdentifier:using:launchHandler:)", which we'll call when it's time for your to actually do that work.

The "sort of" part is that it's possible this was working because this is specifically for app refresh. If the old background app refresh delegate was in place, it's likely that it fired as well, masking the issue.

In any case, if you set everything up properly (see above), then I think this crash will go away.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Thanks for the response. I am still a bit confused however. My understanding is that the .backgroundTask modifier is the replacement for BGTaskScheduler.register in SwiftUI. And the code that runs during the background task is part of that modifier. As per the WWDC22 video Efficiency awaits: Background tasks in SwiftUI, it is used to register the handler.

Also worth noting that the video uses a short ID similar to what I did.

If I take out the modifier, the app crashes with this message:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'No launch handler registered for task with identifier notifications'

Which indicates that the modifier is indeed used for registering the handler.

There's no difference to this behaviour whether I use the short ID notifications or the reverse DNS com.ip18.SubManager.notifications. If the modifier is not there then the app crashes.

Interestingly when I simply killed and ran my app from Xcode multiple times, I eventually came across the All launch handlers must be registered before application finishes launching crash in one of those launches. Which is probably what the crash reports are about.

Attached a symbolicated crash report from one of those Xcode sessions to FB16595418. If I'm understanding this right, it looks like BGTaskScheduler registerForTaskWithIdentifier is already being called internally by the modifier.

Thread 2 Crashed:
0   libsystem_kernel.dylib        	       0x1ef16f1dc __pthread_kill + 8
1   libsystem_pthread.dylib       	       0x2281a3b40 pthread_kill + 268
2   libsystem_c.dylib             	       0x1a5f9c234 __abort + 132
3   libsystem_c.dylib             	       0x1a5f9c1b0 abort + 136
4   libc++abi.dylib               	       0x2280cd5a0 abort_message + 132
5   libc++abi.dylib               	       0x2280bbf10 demangling_terminate_handler() + 344
6   libobjc.A.dylib               	       0x19b560e48 _objc_terminate() + 156
7   libc++abi.dylib               	       0x2280cc8b4 std::__terminate(void (*)()) + 16
8   libc++abi.dylib               	       0x2280cfe1c __cxxabiv1::failed_throw(__cxxabiv1::__cxa_exception*) + 88
9   libc++abi.dylib               	       0x2280cfdc4 __cxa_throw + 92
10  libobjc.A.dylib               	       0x19b55ee74 objc_exception_throw + 448
11  Foundation                    	       0x19d3a6990 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 288
12  BackgroundTasks               	       0x236e9d6ec -[BGTaskScheduler _unsafe_registerForTaskWithIdentifier:usingQueue:launchHandler:] + 448
13  BackgroundTasks               	       0x236e9d4ec -[BGTaskScheduler registerForTaskWithIdentifier:usingQueue:launchHandler:] + 128
14  SwiftUI                       	       0x1a2b77b4c -[BGTaskSchedulerProxy registerForTaskWithIdentifier:launchHandler:] + 220
15  SwiftUI                       	       0x1a34a8028 closure #1 in AppRefreshBackgroundTask.register() + 344
16  SwiftUI                       	       0x1a27cbe15 <deduplicated_symbol> + 1
17  SwiftUI                       	       0x1a293c239 <deduplicated_symbol> + 1
18  SwiftUI                       	       0x1a27ac829 <deduplicated_symbol> + 1
19  libswift_Concurrency.dylib    	       0x1a9adbcc9 completeTaskWithClosure(swift::AsyncContext*, swift::SwiftError*) + 1

My understanding is that the .backgroundTask modifier is the replacement for BGTaskScheduler.register in SwiftUI.

Sigh... yes, I'd forgotten about that API.

If I'm understanding this right, it looks like BGTaskScheduler registerForTaskWithIdentifier is already being called internally by the modifier.

Yes. More specifically, "_unsafe_registerForTaskWithIdentifier" does two different checks when it runs:

  1. It checks how far the app is into the launch process and, if it's to late in that process, it crashes with "All launch handlers must be registered before application finishes launching".

  2. It checks if the ID has already been registered and, if it has, it crashes with "Launch handler for task with identifier <identifier> has already been registered".

...and you're obviously crashing on #1. Moving to here:

Interestingly when I simply killed and ran my app from Xcode multiple times, I eventually came across the All launch handlers must be registered before application finishes launching crash in one of those launches. Which is probably what the crash reports are about.

Based on the evidence at hand, I suspect the issue is that SwiftUI is sometimes registering the crash later than it should, triggering #1. That leads to the most direct solution (assuming you won't want to wait for a bug fix), which would be to stop using ".backgroundTask" and directly use BGTaskScheduler.register.

Also, as a quick clarification here:

.backgroundTask modifier is the replacement for BGTaskScheduler.register in SwiftUI.

Thinking of it as a replacement is somewhat misleading. SwiftUI's architecture is actually trying to provide a unified entry point across multiple frameworks for use cases that are particularly relevant to the interface. It does support BGAppRefreshTask, but it does NOT support the other task types (BGProcessingTask and BGHealthResearchTaskRequest) and, more to the point, all of other task types are totally unrelated to the BackgroundTask framework.

The reason for this is that it isn't actually trying to provide a true replacement. It support BGAppRefreshTask because the primary use case for BGAppRefreshTask is to update the user interface so that it remains current. That's the same reason it DOESN'T support BGProcessingTask. BGProcessingTask is intended to be used for work which requires extended runtime ("several minutes"), which is exactly the kind of work that wouldn't/shouldn't be directly tied to the user interface. Similarly, if your app refresh work is primarily tied to the "backend" of your app, it can be better to use the original register(forTaskWithIdentifier:using:launchHandler:) API instead of artificially creating a connection in SwiftUI that you wouldn't otherwise need.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

I have a similar issue. In my case, it crashes when submitting the request ( BGTaskScheduler.shared.submit(request) ) And it's still valid with the last iOS update: 18.4 (22E5216h) Our task identifiers are long and in the reverse-domain style. It seems, that the new bata has some multithreading issue with the task registration.

I have the same issue, it's very intermittent but more users are experiencing this crash.

If we were to use the temporary fix to call BGTaskScheduler.shared.register(forTaskWithIdentifier:) directly, where would we use this in the SwiftUI app lifecycle? Is it in the .init of the App?

Edit: I tried this and it still crashes.

If we were to use the temporary fix to call BGTaskScheduler.shared.register(forTaskWithIdentifier:) directly, where would we use this in the SwiftUI app lifecycle? Is it in the .init of the App?

Theoretically init would probably work, however, my actual recommendation would be that you use UIApplicationDelegateAdaptor and then implement application(_:didFinishLaunchingWithOptions:) and call it there. The core issue here comes down to timing issues within SwiftUI's lifecycle, so I think it's worth being certain that any workaround cannot have similar timing issues.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

@DTS Engineer I ran some tests here that I think maybe highlights what's going on:

  1. I set a symbolic breakpoint on -[BGTaskScheduler registerForTaskWithIdentifier:usingQueue:launchHandler:]
  2. I edited my app scheme to "Wait for the executable to be launched"
  3. I run the app from Xcode.
  4. I trigger the app to be launched from a home screen widget (AudioPlaybackIntent)

Here, on iOS 18.3 and earlier, at the time the app is launched, the symbolic breakpoint is triggered. On 18.4 beta, the symbolic breakpoint is not triggered.

  1. Now, foreground the app. On iOS 18.4 beta the symbolic breakpoint is triggered now, but because the app was already launched (in the background) the assert kicks in.

I hope you guys are already on track to fix this issue (but it's worrying that we've now seen 4 betas without a fix). I don't think widgets are the only way to trigger this issue. I've seen it happen on the first launch right after installing a new build from TestFlight. Maybe app pre-warming could be another path to trigger the launch without tasks getting registered, but that's just speculation.

__

Jonas Salling

BGTaskScheduler crashes on iOS 18.4
 
 
Q