Fix: SwiftData ModelContainer Hides Core Data Migration Error on iOS 26

Fix: SwiftData ModelContainer Hides Core Data Migration Error on iOS 26

Written by

in

Quick answer: On iOS 26 and earlier, SwiftData’s ModelContainer throws SwiftDataError.loadIssueModelContainer with an empty userInfo dictionary, hiding the underlying Core Data NSError. The only reliable way to get the real migration error (entity, attribute, reason) is to preflight the store with NSPersistentStoreCoordinator before creating the ModelContainer. On iOS 27+, the error is stored internally but remains inaccessible via public API.

Why this happens

When ModelContainer fails to open a persistent store — typically due to a schema migration mismatch — SwiftData wraps the failure in SwiftDataError.loadIssueModelContainer. On iOS 26 and earlier, this error carries no userInfo, no underlying error, and no public property that exposes the original NSCocoaErrorDomain failure. The detailed Core Data diagnostics (entity name, missing attribute, validation reason) are printed to the console but discarded before the error reaches your catch block.

Starting with iOS 27, the SwiftData runtime retains an internal _underlyingCocoaError on the error instance, but Apple has not added a public accessor for it. Reflection (Mirror) can reach that property in the current implementation, but it is an implementation detail and may change without notice. Therefore, there is no supported, future-proof way to extract the Core Data error from a failed ModelContainer on any shipping OS version.

The root cause is a deliberate abstraction boundary: SwiftData presents its own error domain (SwiftData.SwiftDataError) and does not surface Core Data’s NSPersistentStoreCoordinator errors through the public API surface.

How to fix it

1. Preflight the store with NSPersistentStoreCoordinator (recommended)

Before you hand the store URL to ModelContainer, open it once with Core Data’s coordinator. The coordinator’s addPersistentStore method throws the full NSError with userInfo intact, including NSUnderlyingErrorKey, the failing entity, and the missing attribute.

import CoreData
import SwiftData

func preflightStore(at url: URL, schema: Schema) throws -> NSError? {
    let mom = NSManagedObjectModel(schema: schema)
    let psc = NSPersistentStoreCoordinator(managedObjectModel: mom)
    do {
        try psc.addPersistentStore(
            ofType: NSSQLiteStoreType,
            configurationName: nil,
            at: url,
            options: [
                NSMigratePersistentStoresAutomaticallyOption: true,
                NSInferMappingModelAutomaticallyOption: true
            ]
        )
        return nil // store opened successfully
    } catch {
        return error as NSError // full Core Data error with userInfo
    }
}

@MainActor
func makeContainer(url: URL, schema: Schema) throws -> ModelContainer {
    if let coreDataError = try preflightStore(at: url, schema: schema) {
        // Report coreDataError to your crash logger here
        print("Core Data migration failed: \(coreDataError)")
        print("Entity: \(coreDataError.userInfo[NSValidationKeyErrorKey] ?? "unknown")")
        // Optionally re-throw a wrapped error that preserves the details
        throw coreDataError
    }
    let config = ModelConfiguration(url: url)
    return try ModelContainer(for: schema, configurations: config)
}

Call makeContainer from your app’s startup task. If the preflight fails, you now have the exact NSCocoaErrorDomain code (e.g., 134110) and the userInfo dictionary that identifies the entity and attribute causing the migration to abort.

2. Mirror reflection on iOS 27+ (unsupported, fallback only)

If you must support iOS 27 and later without the Core Data preflight, you can use Mirror to read the private _underlyingCocoaError property. Treat this as a last resort and guard it with an OS version check.

import Foundation

func underlyingCocoaError(from error: Error) -> NSError? {
    guard #available(iOS 27.0, *) else { return nil }
    let mirror = Mirror(reflecting: error)
    for child in mirror.children {
        if child.label == "_underlyingCocoaError",
           let cocoaError = child.value as? CocoaError {
            return cocoaError._nsError
        }
    }
    return nil
}

// Usage in your catch block:
catch {
    if let coreDataError = underlyingCocoaError(from: error) {
        // Log coreDataError.userInfo
    }
    // Fall back to generic handling
}

Warning: This relies on a private underscore-prefixed property. It will break if Apple renames or removes the field in a future SDK or OS update. Do not ship this as your primary error-reporting path.

3. Catch the console output (not recommended for production)

The detailed Core Data error is logged to the system console via os_log. You could attach a OSLogSubscriber or redirect stderr in a debug build, but this is fragile, asynchronous, and unsuitable for crash reporting in released apps. Mentioned only for completeness during local debugging.

How to prevent it

  • Always provide default values for new non-optional attributes in schema migrations, or supply a custom NSEntityMigrationPolicy that populates them.
  • Test migrations against real user data before shipping a schema bump. Use the preflight helper above in your CI pipeline.
  • Version your schemas explicitly with VersionedSchema and increment the version identifier for every change.
  • Enable lightweight migration options (NSMigratePersistentStoresAutomaticallyOption, NSInferMappingModelAutomaticallyOption) both in the preflight and in ModelConfiguration so the behavior matches.
  • Log the preflight error to your telemetry backend with the full userInfo so you can correlate migration failures with specific schema versions.

FAQ

Why does SwiftData hide the Core Data error on iOS 26?

SwiftData defines its own error domain (SwiftData.SwiftDataError) and maps all store-loading failures to loadIssueModelContainer. The underlying NSCocoaErrorDomain error is logged to the console but not attached to the thrown error’s userInfo or exposed via a public property on those OS versions.

Can I use NSUnderlyingErrorKey on the SwiftData error?

No. On iOS 26 and earlier, (error as NSError).userInfo[NSUnderlyingErrorKey] is nil. The key is only populated on iOS 27+ inside the private _underlyingCocoaError, which has no public accessor.

Is the NSPersistentStoreCoordinator preflight guaranteed to match ModelContainer’s behavior?

Yes, provided you use the same NSManagedObjectModel (built from the same Schema), the same store URL, and the same migration options (NSMigratePersistentStoresAutomaticallyOption and NSInferMappingModelAutomaticallyOption). The preflight exercises the exact same Core Data stack that ModelContainer uses internally.

Image query

“developer laptop debugging code”

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.