SwiftData Fatal error: Editors must register their identifiers before invoking operations on this store

I have a UIKit app where I've adopted SwiftData and I'm struggling with a crash coming in from some of my users. I'm not able to reproduce it myself and as it only happens to a small fraction of my user base, it seems like a race condition of some sort.

This is the assertion message: SwiftData/DefaultStore.swift:453: Fatal error: API Contract Violation: Editors must register their identifiers before invoking operations on this store SwiftData.DefaultStore: 00CF060A-291A-4E79-BEC3-E6A6B20F345E did not. (ID is unique per crash)

This is the ModelActor that crashes:

@available(iOS 17, *)
@ModelActor
actor ConsumptionDatabaseStorage: ConsumptionSessionStorage {
	struct Error: LocalizedError {
		var errorDescription: String?
	}
	
	private let sortDescriptor = [SortDescriptor(\SDConsumptionSession.startTimeUtc, order: .reverse)]
	
	static func createStorage(userId: String) throws -> ConsumptionDatabaseStorage {		
		guard let appGroupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: UserDefaults.defaultAppGroupIdentifier) else {
			throw Error(errorDescription: "Invalid app group container ID")
		}
		
		func createModelContainer(databaseUrl: URL) throws -> ModelContainer {
			return try ModelContainer(for: SDConsumptionSession.self, SDPriceSegment.self, configurations: ModelConfiguration(url: databaseUrl))
		}
		
		let databaseUrl = appGroupContainer.appendingPathComponent("\(userId).sqlite")
		do {
			return self.init(modelContainer: try createModelContainer(databaseUrl: databaseUrl))
		} catch {
			// Creating the model storage failed. Remove the database file and try again.
			try? FileManager.default.removeItem(at: databaseUrl)
			return self.init(modelContainer: try createModelContainer(databaseUrl: databaseUrl))
		}
	}
	
	func isStorageEmpty() async -> Bool {
		(try? self.modelContext.fetchCount(FetchDescriptor<SDConsumptionSession>())) ?? 0 == 0 // <-- Crash here!
	}
	
	func sessionsIn(interval: DateInterval) async throws -> [ConsumptionSession] {
		let fetchDescriptor = FetchDescriptor(predicate: #Predicate<SDConsumptionSession> { sdSession in
			if let startDate = sdSession.startTimeUtc {
				return interval.start <= startDate && interval.end > startDate
			} else {
				return false
			}
		}, sortBy: self.sortDescriptor)
		let consumptionSessions = try self.modelContext.fetch(fetchDescriptor) // <-- Crash here!
		return consumptionSessions.map { ConsumptionSession(swiftDataSession: $0) }
	}
	
	func updateSessions(sessions: [ConsumptionSession]) async throws {
		if #unavailable(iOS 18) {
			// Price segments are duplicated if re-inserted so unfortunately we have to delete and reinsert sessions.
			// On iOS 18, this is enforced by the #Unique macro on SDPriceSegment.
			let sessionIds = Set(sessions.map(\.id))
			try self.modelContext.delete(model: SDConsumptionSession.self, where: #Predicate<SDConsumptionSession> {
				sessionIds.contains($0.id)
			})
		}
		for session in sessions {
			self.modelContext.insert(SDConsumptionSession(consumptionSession: session))
		}
		if self.modelContext.hasChanges {
			try self.modelContext.save()
		}
	}
	
	func deleteAllSessions() async {
		if #available(iOS 18, *) {
			try? self.modelContainer.erase()
		} else {
			self.modelContainer.deleteAllData()
		}
	}
}

The actor conforms to this protocol:

protocol ConsumptionSessionStorage {
	func isStorageEmpty() async -> Bool
	func hasCreditCardSessions() async -> Bool
	func sessionsIn(interval: DateInterval) async throws -> [ConsumptionSession]
	func updateSessions(sessions: [ConsumptionSession]) async throws
	func deleteAllSessions() async
}

The crash is coming in from line 30 and 41, in other words, when trying to fetch data from the database. There doesn't seem to be any common trait for the crashes. They occur across iOS versions and device types.

Any idea what might cause this?

Answered by selsoe in 855238022

Update: I managed to reproduce the crash and solve it (I think). I had a scenario where the ModelActor in some cases would leak and stay transient after the user logged out. If another user logged in afterwards and another instance of the ModelActor would be created, this crash would occur. After I fixed the leak, the app seemingly no longer crashes.

It's still a bit of a mystery to me, why it would crash though. The ModelActors are initialized with their own user-specific database, so they are completely distinct. My theory is that the leaked database could reference some resources that no longer existed after the user logged out and that caused the crash.

Could it be that you create multiple instances of the ModelContainer?

Do you have a full symbolicated crash report to share? You can include crash reports directly in your post using the forums attachment feature.

For any random crash related to SwiftData / Core Data, I'd suggest that you add com.apple.CoreData.ConcurrencyDebug 1 as a launch argument of the app to check if there is any violation, as discussed here.

Also, I guess ConsumptionSession is Sendable? Otherwise, Xcode will give you an error if you compile with Swift 6. If you haven't used Swift 6, I'd suggest that you give it a try to see if that unveils any race condition.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

@DTS Engineer thank you for these suggestions.

I've added the full crash report from Crashlytics to this post.

After enabling the launch argument you suggested, I managed to reproduce the crash in the debugger once, so that's some progress.

After enabling Swift 6 and complete concurrency checking, I got some build errors in the class owning the ModelActor. Wherever I accessed the ModelActor, I got this error:

Sending 'self.localStorage.some' risks causing data races.

As mentioned in my original post, the ModelActor conforms to a protocol because I am using another type of storage in iOS 16 which doesn't support SwiftData. This protocol didn't require that conforming types were Actors, leading me to suspect that the class owning the ModelActor didn't treat it as an actor, because it only know about the protocol. Adding the Actor requirement to the protocol, removed the build errors in the owning class.

Now, I have to ship this change to confirm that it actually fixes it, since I cannot reliably reproduce the crash, but it seems like a good candidate for a fix, wouldn't you say?

Thank you for your help!

Accepted Answer

Update: I managed to reproduce the crash and solve it (I think). I had a scenario where the ModelActor in some cases would leak and stay transient after the user logged out. If another user logged in afterwards and another instance of the ModelActor would be created, this crash would occur. After I fixed the leak, the app seemingly no longer crashes.

It's still a bit of a mystery to me, why it would crash though. The ModelActors are initialized with their own user-specific database, so they are completely distinct. My theory is that the leaked database could reference some resources that no longer existed after the user logged out and that caused the crash.

I had the same crash happening, i managed to fix it. let me explain my use case, maybe it helps someone.

I had a local database and a cloud database. The user could choose to migrate from local to cloud. During this process, I sometimes got OPs crash. I had something like this:

local = try ModelContainer.local()
cloud = try ModelContainer.cloud()

try migrate(toCloud: true)
try local.erase()
//after migration, I erased the local instance.
local = try ModelContainer.local() // << the fix
// reinitializing the local container fixed the crash.
SwiftData Fatal error: Editors must register their identifiers before invoking operations on this store
 
 
Q