Introduction

Core Data's NSManagedObjectContext is not thread-safe. Each context is bound to a specific dispatch queue, and accessing managed objects from the wrong queue triggers a concurrency violation. In debug mode, this logs a warning. In production, it causes unpredictable crashes, data corruption, and lost saves.

Symptoms

  • Console warning: CoreData: warning: Multithreading CONVIOLATION
  • NSManagedObjectContext accessed from multiple threads
  • Random crashes during context.save()
  • Data appears corrupted or duplicated after save
  • Fault firing fails with NSObjectInaccessibleException

Example warning: `` CoreData: warning: A NSManagedObjectContext was used from a thread other than the one it was created on. This is not supported and will result in data corruption. Context: <NSManagedObjectContext: 0x600001234567>, thread: <NSThread: 0x600000111111>, created on thread: <NSThread: 0x600000222222>

Common Causes

  • Passing NSManagedObject instances between threads instead of NSManagedObjectID
  • Accessing context properties from a background callback without perform
  • Using NSMainQueueConcurrencyType context from a background queue
  • SwiftUI @FetchRequest triggering access on background thread
  • Completion handlers executing on arbitrary GCD queues

Step-by-Step Fix

  1. 1.Pass object IDs between threads, not managed objects:
  2. 2.```swift
  3. 3.// WRONG: passing the object itself
  4. 4.func processOnBackground(user: User) {
  5. 5.DispatchQueue.global().async {
  6. 6.// user was created on main queue - CONVIOLATION!
  7. 7.print(user.name)
  8. 8.}
  9. 9.}

// CORRECT: pass the object ID func processOnBackground(userID: NSManagedObjectID) { let backgroundContext = persistentContainer.newBackgroundContext() backgroundContext.perform { guard let user = try? backgroundContext.existingObject(with: userID) as? User else { return } print(user.name) // Safe: user was fetched on this context's queue } } ```

  1. 1.Use perform for all context access:
  2. 2.```swift
  3. 3.let context = persistentContainer.newBackgroundContext()
  4. 4.context.perform {
  5. 5.// All Core Data operations happen inside perform block
  6. 6.let fetchRequest: NSFetchRequest<User> = User.fetchRequest()
  7. 7.let users = try? context.fetch(fetchRequest)

for user in users ?? [] { user.lastSyncDate = Date() }

try? context.save() } ```

  1. 1.Update UI from main queue context:
  2. 2.```swift
  3. 3.// Background context does the work
  4. 4.let backgroundContext = persistentContainer.newBackgroundContext()
  5. 5.backgroundContext.perform {
  6. 6.// Fetch and process on background
  7. 7.let items = try? backgroundContext.fetch(request)

// Save changes try? backgroundContext.save()

// Update UI on main queue DispatchQueue.main.async { // Use main context for UI self.mainContext.perform { let refreshedItems = try? self.mainContext.fetch(request) self.tableView.reloadData() } } } ```

  1. 1.Use parent-child context hierarchy:
  2. 2.```swift
  3. 3.func createBackgroundContext() -> NSManagedObjectContext {
  4. 4.let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
  5. 5.context.parent = persistentContainer.viewContext // View context is parent
  6. 6.context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
  7. 7.return context
  8. 8.}

// Save propagates to parent backgroundContext.perform { // Make changes try? backgroundContext.save() // Pushes to parent (viewContext)

// Save parent to persist to store self.persistentContainer.viewContext.perform { try? self.persistentContainer.viewContext.save() } } ```

Prevention

  • Set context.shouldDeleteInaccessibleFaults = true to catch issues early
  • Enable -com.apple.CoreData.ConcurrencyDebug 1 in launch arguments during development
  • Always use perform or performAndWait for context access
  • Never store NSManagedObject references across thread boundaries
  • Use NSPersistentContainer.newBackgroundContext() for background work
  • In SwiftUI, use @FetchRequest which automatically uses the view context on the main queue