SwiftData: Solving Fatal Errors and EXC_BAD_ACCESS While Handling Entities on Different Threads

SwiftData: Solving Fatal Errors and EXC_BAD_ACCESS While Handling Entities on Different Threads

When SwiftData was first announced I considered leaving it alone before realizing this may be the best time to switch over. That said, I've had some difficulties getting the same functionalities working, like my earlier post where I had issues filtering the browse lists by an entity relationship, but having my app crash at random-seeming parts when creating entities on background threads was, for me, the most opaque and frustrating one yet... at least for now. And, once again, like all issues it was resolved with a simple solution.

Pinterest geared image showing my post title, images from below, and my main URL.

Back Story

Part of my back end code is written in a class that is composed fully of asynchronous functions that I call awaiting the returned results. Previously with CoreData I pass in a main entity and the data context to the method where new entities, all related and connected to that main entity, are created and saved.

This has worked well in the past although it didn't act consistently once I updated my code to the new SwiftData methodology and dropped the data context as the ModelContext could be found through that passed in main entity.

But First Concurrency

First a quick aside in case you're also looking to speed up your code. To speed up my code I ended up using two posts I found on Donny's blog. The first post, which I started out on, used async let to run your steps serially rather than concurrently while the second post was all about running tasks in parallel with task groups. I came across these while trying to solve my problem and although the new code compiled and ran so much quicker my issues weren't fixed and, in fact, it even broke my partial (and temporary) fix of adding a simple sleep command. I used the sleep command to prevent the race conditions I thought, at first, were causing the issues.

Image is a screenshot of the top the Running tasks in parallel with Swift Concurrency’s task groups post showing the website header, post cover image, and title.
Screenshot of the top of the Running tasks in parallel with Swift Concurrency’s task groups post that I ended going with was taken on December 10th, 2023.

Word of warning: one spot I updated didn't work with task groups (didn't create all the needed entities) even once I fixed the main issue so I went back to the working async let there and it then worked beautifully. The other spot I used task groups is still set up and working perfectly though.

In addition to those posts I also found Swift Senpai's post Understanding Swift Task Groups With Example that looked great at a glance in case you want another explanation.

Image is a screenshot of the top the Swift Senpai's post Understanding Swift Task Groups With Example showing the website header, post cover image, and title.
Screenshot of the top of the Understanding Swift Task Groups With Example post was taken on December 10th, 2023.

The Problems

EXC_BAD_ACCESS

The first error I had was a generalized EXC_BAD_ACCESS (code=1, address=...) that showed up mainly in my entry App @Main struct but also popped up at various entity relationships spots all under various thread count numbers.

Screenshot of my code showing the failed EXC_BAD_ACCESS error.
One instance of thread 12 failing with bad access on the user relationship.

I vaguely remembered a list of common SwiftData errors on Hacking with Swift so I tracked down his post Common SwiftData errors and their solutions where he sums up the EXC_BAD_ACCESS (code=1, address=0x0) well in the very first sentence with:

"If this occurs you're in trouble, because it could mean a whole range of things."

He then went on to describe how to fix this issue when it shows up in a predicate which didn't help me here or with my actual current, at that time, predicate issue.

Text explaining how if EXC_BAD_ACCESS "occurs you're in trouble, because it could mean a whole range of things" and then mentioning how to fix it if it's in a Predicate.
Screenshot taken from Hacking with Swift's Common SwiftData errors and their solutions on December 6th, 2023.

When further researching this I came across Antoine Van Der Lee's blog SwiftLee that explained using an address sanitizer once the source of the EXC_BAD_ACCESS was found locally along with another post on using the thread sanitizer to deal with data races. These couldn't be used at the same time and I found they didn't personally help me as anytime I had one of them enabled my app performed beautifully. Figured I'd share this with you though in case it could help you.

Fatal Error: Duplicate Keys

At some point in all of this when I couldn't figure out how to fix the issue I decided to at least help mitigate it by manually saving the ModelContext after each related entity was done being created. I figured this way if the program crashed at least the data that had been created before that point would be saved and the user wouldn't have to start back at the beginning each time. This didn't help as once I added saving, at any of the points, I got fatal errors on my entities complaining about duplicate keys.

Fatal error: Duplicate keys of type 'EntityName' were found in a Dictionary.
This usually means either that the type violates Hashable's requirements, or that members of such a dictionary were mutated after insertion.

The Solution

Path to Solution

The StackOverflow question titled SwiftData insert crashes with EXC_BAD_ACCESS using background thread from ModelActor got me to where I needed to be. That said, I need to be completely honest and share that I didn't read the code section of the question and the approved answer was just scanned. What helped me specifically was the solution added to bottom of the question and in that solution mainly just the first sentence: "You have to create the ModelActor on a different thread than the main tread."

Image shows the solution part of the StackOverflow question along with its tags, two comments, and information about who asked it.
Screenshot taken from StackOverflow question SwiftData insert crashes with EXC_BAD_ACCESS using background thread from ModelActor on December 6th, 2023.

This line immediately reminded me of Hacking with Swift's post How to discard changes to a SwiftData object where he showed how to make an entity editable in a way where you can choose when to save or discard any changes instead of the default autosaving. Earlier I had followed this code to implement it in my edit views so a user can roll everything back if they choose to cancel (by just dismissing it all) or keep the changes by saving the ModelContext manually.

Image shows two code sections on a dark background alongside text in the middle.
Screenshot of Hacking with Swift's solution was taken on December 10th, 2023 from his How to discard changes to a SwiftData object post.

Solution With Example

With this idea I decided to update the problematic functions so rather than passing in the main entity itself I instead pass in the ModelContainer and the entity's PersistentIdentifier so it can be recreated within a new ModelContext for just this thread.

Assuming you're using async let I created a quick code example to show how I fixed my problem. This also worked in my function using TaskGroup too.

In this example I have a main function where I pass in my main entity (to connect with any other created entities) and an array of information to go through. This method just iterates through the array and calls the asynchronous method making sure to pass the entity by its PersistentModelID and send in the ModelContainer.

public func mainEntryMethod(entity: EntityName, arrayToGoThrough: [objectType]) async {
   for thisObject in arrayToGoThrough {
      async let _ = createEntities(in: entity.modelContext!.container, entityID: entity.persistentModelID, thisObject: objectType)
   }
}

This asynchronous method is where the magic happens. Here the main entity is recreated in a new ModelContext based on the passed in ModelContainer and if it can't be recreated I exit the method. If it works I use the information passed in to create what I originally wanted before saving the whole thing and returning.

private func createEntities(in container: ModelContainer, entityID: PersistentIdentifier, objectInfo: objectType) async {
   // First recreate the main entity in it's own ModelContext
   let modelContext = ModelContext(container)
   modelContext.autosaveEnabled = false
   let entity = modelContext.model(for: entityID) as? EntityName
   if entity == nil {
      // Can't continue without entity
      // TODO: Handle any error handling and...
      return
   }

   // ... creating the related entities and attaching them   

   do {
      try modelContext.save()
   } catch {
      // It can't be saved
      // TODO: Handle any error handling and...
      return
   }
   // Success!
}

With the different ModelContext used in my problematic asynchronous methods both my fatal and bad access errors are gone!

I hope this solution helps you solve your problem. If it didn't quite get you all the way I'd love to hear, in the comments below, how you solved it! Maybe your solution can help someone else!

I hope you’re having a good day.


If you’re interested in getting any of my future blog updates I normally share them to my Facebook page and Instagram account. You’re also more than welcome to join my email list located right under the search bar or underneath this post.



Related Posts

Latest Posts