fetchExisting() crashes the app when object is not found #90

Closed
opened 2025-12-29 15:24:14 +01:00 by adam · 11 comments
Owner

Originally created by @mkadan on GitHub (Sep 29, 2016).

please check DataStack+Querying.swift and BaseDataTransaction+Querying.swift

Four fetchExisting() methods there have the "as!" cast, e.g.:

return (try self.context.existingObjectWithID(object.objectID) as! T)

This cast crashes the app if nil is casted (that may happen if the object is not available in the context), and this crash if not catched, as this is a fatal crash.

existingObjectWithID
If the object cannot be fetched, or does not exist, or cannot be faulted, it returns nil.

Why not use "as?" instead?


A bit another, though related issue is if the object was never saved, then the app crashes with:

2016-09-29 12:19:17.898 LayfootakUserApp[55561:7950554] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'nil is not a valid object ID'

In my opinion it should not crash and just return nil.
Could easily be replacing it with this in singe object fetchExisting() methods:

return (object.objectID.persistentStore != nil) ? (try self.context.existingObjectWithID(object.objectID) as? T) : nil

and with this in multi object fetchExisting() methods:

return objects.flatMap { ($0.objectID.persistentStore != nil) ? (try? self.mainContext.existingObjectWithID($0.objectID)) as? T : nil }


Here I attach fixed sources that resolve these two issues:
fixed-sources.zip

Originally created by @mkadan on GitHub (Sep 29, 2016). please check DataStack+Querying.swift and BaseDataTransaction+Querying.swift Four fetchExisting() methods there have the "as!" cast, e.g.: > return (try self.context.existingObjectWithID(object.objectID) as! T) This cast crashes the app if nil is casted (that may happen if the object is not available in the context), and this crash if not catched, as this is a fatal crash. > existingObjectWithID > If the object cannot be fetched, or does not exist, or cannot be faulted, it returns nil. Why not use "as?" instead? --- A bit another, though related issue is if the object was never saved, then the app crashes with: > 2016-09-29 12:19:17.898 LayfootakUserApp[55561:7950554] **\* Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'nil is not a valid object ID' In my opinion it should not crash and just return nil. Could easily be replacing it with this in singe object fetchExisting() methods: > return (object.objectID.persistentStore != nil) ? (try self.context.existingObjectWithID(object.objectID) as? T) : nil and with this in multi object fetchExisting() methods: > return objects.flatMap { ($0.objectID.persistentStore != nil) ? (try? self.mainContext.existingObjectWithID($0.objectID)) as? T : nil } --- Here I attach fixed sources that resolve these two issues: [fixed-sources.zip](https://github.com/JohnEstropia/CoreStore/files/500483/fixed-sources.zip)
adam added the user bug label 2025-12-29 15:24:14 +01:00
adam closed this issue 2025-12-29 15:24:15 +01:00
Author
Owner

@JohnEstropia commented on GitHub (Sep 29, 2016):

@mkadan The documentation you are referring to is for Objective-C

If the object cannot be fetched, or does not exist, or cannot be faulted, it returns nil.

In Objective-C, errors are returned from methods by returning nil and populating the error parameter:

- (__kindof NSManagedObject *)existingObjectWithID:(NSManagedObjectID *)objectID 
                                             error:(NSError * _Nullable *)error;

In Swift, this method does not return an optional, and so all nil returns will be bridged by swift as a throw:

func existingObject(with objectID: NSManagedObjectID) throws -> NSManagedObject

existingObject() 's documentation:
https://developer.apple.com/reference/coredata/nsmanagedobjectcontext/1506686-existingobject

The as! T casting done by CoreStore just casts the non-optional NSManagedObject to T.

A bit another, though related issue is if the object was never saved, then the app crashes with:
2016-09-29 12:19:17.898 LayfootakUserApp[55561:7950554] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'nil is not a valid object ID'

None of CoreStore's fetchExisting() methods accepts an optional. So if you are passing nil there then Swift has nothing else to do but crash. I am guessing that a nil value from your Objective-C code is getting through Swift bridged code, so you should start to debug where that happens.

@JohnEstropia commented on GitHub (Sep 29, 2016): @mkadan The documentation you are referring to is for Objective-C ``` If the object cannot be fetched, or does not exist, or cannot be faulted, it returns nil. ``` In Objective-C, errors are returned from methods by returning `nil` and populating the `error` parameter: ``` objc - (__kindof NSManagedObject *)existingObjectWithID:(NSManagedObjectID *)objectID error:(NSError * _Nullable *)error; ``` In Swift, this method does not return an optional, and so all `nil` returns will be bridged by swift as a `throw`: ``` swift func existingObject(with objectID: NSManagedObjectID) throws -> NSManagedObject ``` `existingObject()` 's documentation: https://developer.apple.com/reference/coredata/nsmanagedobjectcontext/1506686-existingobject The `as! T` casting done by CoreStore just casts the non-optional `NSManagedObject` to `T`. > A bit another, though related issue is if the object was never saved, then the app crashes with: > 2016-09-29 12:19:17.898 LayfootakUserApp[55561:7950554] **\* Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'nil is not a valid object ID' None of CoreStore's `fetchExisting()` methods accepts an optional. So if you are passing `nil` there then Swift has nothing else to do but crash. I am guessing that a `nil` value from your Objective-C code is getting through Swift bridged code, so you should start to debug where that happens.
Author
Owner

@mkadan commented on GitHub (Sep 29, 2016):

Ok, I see your point for the issue N1, and if it works like this, that's ok for me.

But please check this sample code for issue N2, it crashes, though in my opinion it should not:

    CoreStore.beginAsynchronous {
        (transaction) in
        let user = User()
        let user$ = transaction.fetchExisting(user)
    }

It may happen not only if the instance was just created but also when it was deleted, like it happens in our app.

@mkadan commented on GitHub (Sep 29, 2016): Ok, I see your point for the issue N1, and if it works like this, that's ok for me. But please check this sample code for issue N2, it crashes, though in my opinion it should not: > ``` > CoreStore.beginAsynchronous { > (transaction) in > let user = User() > let user$ = transaction.fetchExisting(user) > } > ``` It may happen not only if the instance was just created but also when it was deleted, like it happens in our app.
Author
Owner

@JohnEstropia commented on GitHub (Sep 29, 2016):

You can't create NSManagedObject's directly through their default initializer. That object won't have a parent NSManagedObjectContext. Use the transaction's create method for this:

CoreStore.beginAsynchronous {
        (transaction) in
    let user = transaction.create(Into<User>())
}
@JohnEstropia commented on GitHub (Sep 29, 2016): You can't create `NSManagedObject`'s directly through their default initializer. That object won't have a parent `NSManagedObjectContext`. Use the `transaction`'s create method for this: ``` swift CoreStore.beginAsynchronous { (transaction) in let user = transaction.create(Into<User>()) } ```
Author
Owner

@mkadan commented on GitHub (Sep 29, 2016):

Ok thanks for your explanation. I tried to create a sample based on our code that fails, but seems it's not that easy to do, as it involves cascaded deletes. I'll provide it later when I extract the problem, of course if that's really a problem in CoreStore.

@mkadan commented on GitHub (Sep 29, 2016): Ok thanks for your explanation. I tried to create a sample based on our code that fails, but seems it's not that easy to do, as it involves cascaded deletes. I'll provide it later when I extract the problem, of course if that's really a problem in CoreStore.
Author
Owner

@mkadan commented on GitHub (Sep 29, 2016):

I was able to reproduce the problem on sample project:

Company has ToMany-DeleteCascade relation to User, User has ToOne-NullifyCascade relation to Company.

    CoreStore.beginSynchronous {
        (transaction) in
        let user$ = transaction.create(Into(User))
        let company$ = transaction.create(Into(Company))

        user$.company = company$

        transaction.commitAndWait()
    }

    let user = CoreStore.fetchOne(From(User))!

    CoreStore.beginSynchronous {
        (transaction) in
        let user$ = transaction.fetchExisting(user)
        transaction.delete(user$)

        transaction.commitAndWait()
    }

    let company = user.company
    // it's objectID is 00000000000000000000 and has no persistenceStore set

    CoreStore.beginSynchronous {
        (transaction) in
        // crashes
        let company$ = transaction.fetchExisting(company)

        transaction.commitAndWait()
    }

using

return (object.objectID.persistentStore != nil) ? (try self.context.existingObjectWithID(object.objectID) as! T) : nil

in fetchExisting() implementation fixes this crash

@mkadan commented on GitHub (Sep 29, 2016): I was able to reproduce the problem on sample project: Company has ToMany-DeleteCascade relation to User, User has ToOne-NullifyCascade relation to Company. > ``` > CoreStore.beginSynchronous { > (transaction) in > let user$ = transaction.create(Into(User)) > let company$ = transaction.create(Into(Company)) > > user$.company = company$ > > transaction.commitAndWait() > } > > let user = CoreStore.fetchOne(From(User))! > > CoreStore.beginSynchronous { > (transaction) in > let user$ = transaction.fetchExisting(user) > transaction.delete(user$) > > transaction.commitAndWait() > } > > let company = user.company > // it's objectID is 00000000000000000000 and has no persistenceStore set > > CoreStore.beginSynchronous { > (transaction) in > // crashes > let company$ = transaction.fetchExisting(company) > > transaction.commitAndWait() > } > ``` using > return (object.objectID.persistentStore != nil) ? (try self.context.existingObjectWithID(object.objectID) as! T) : nil in fetchExisting() implementation fixes this crash
Author
Owner

@JohnEstropia commented on GitHub (Sep 29, 2016):

Thats expected behavior. You accessed an object that is supposedly deleted. My guess is that user.community is supposed to return nil, but the Swift compiler cannot infer that for you during compile time, so you end up with a broken 0-object. In any case, CoreStore can't protect you from accessing deleted objects directly like this, as that is Core Data's internal behavior

@JohnEstropia commented on GitHub (Sep 29, 2016): Thats expected behavior. You accessed an object that is supposedly deleted. My guess is that user.community is supposed to return nil, but the Swift compiler cannot infer that for you during compile time, so you end up with a broken 0-object. In any case, CoreStore can't protect you from accessing deleted objects directly like this, as that is Core Data's internal behavior
Author
Owner

@mkadan commented on GitHub (Sep 29, 2016):

But is it possible to add this check to CoreStore just as a workaround for CoreData bug?

@mkadan commented on GitHub (Sep 29, 2016): But is it possible to add this check to CoreStore just as a workaround for CoreData bug?
Author
Owner

@JohnEstropia commented on GitHub (Sep 29, 2016):

That's not exactly a Core Data bug. It is the responsibility of the app to design an architecture so this doesn't happen. If anything, CoreStore should be guarding against this with an assert(!object.isDeleted), because tolerating this will most likely just cause another problem somewhere else. On iOS 8 and below, your code will probably already crash on the line

 let company = user.company

(iOS 9's new shouldDeleteInaccessibleFaults just keeps the object up for a while)

In any case, I suggest that this be handled on the app side.

@JohnEstropia commented on GitHub (Sep 29, 2016): That's not exactly a Core Data bug. It is the responsibility of the app to design an architecture so this doesn't happen. If anything, CoreStore should be guarding against this with an `assert(!object.isDeleted)`, because tolerating this will most likely just cause another problem somewhere else. On iOS 8 and below, your code will probably already crash on the line ``` let company = user.company ``` (iOS 9's new `shouldDeleteInaccessibleFaults` just keeps the object up for a while) In any case, I suggest that this be handled on the app side.
Author
Owner

@mkadan commented on GitHub (Sep 29, 2016):

Thanks for your answer. I think we will try to review our code to exclude such cases from it, and if it is not possible for some reason, will write an extension method to CoreStore transaction to perform this check outside of the core library functionality and test it carefully for our app, as you have a point that it may break something else inside the CoreStore.
From your side, is it possible you include this assert with some reasonable message, so that users may know what actually caused the crash?

@mkadan commented on GitHub (Sep 29, 2016): Thanks for your answer. I think we will try to review our code to exclude such cases from it, and if it is not possible for some reason, will write an extension method to CoreStore transaction to perform this check outside of the core library functionality and test it carefully for our app, as you have a point that it may break something else inside the CoreStore. From your side, is it possible you include this assert with some reasonable message, so that users may know what actually caused the crash?
Author
Owner

@JohnEstropia commented on GitHub (Sep 29, 2016):

We could do that, but iOS 9 should already be giving you a warning log when this happens. Also, on iOS 8 your app will be crashing with the original exception anyway.

@JohnEstropia commented on GitHub (Sep 29, 2016): We could do that, but iOS 9 should already be [giving you a warning log](http://stackoverflow.com/a/32503486/809614) when this happens. Also, on iOS 8 your app will be crashing with the original exception anyway.
Author
Owner

@JohnEstropia commented on GitHub (Sep 29, 2016):

I'll experiment for a while with that assert(). If it changes the current behavior of fetchExisting(), I might not put it in (example, if there are previous cases where isDeleted is marked but goes through fetchExisting() returning nil, adding an assert() will be a breaking change for those use cases)

@JohnEstropia commented on GitHub (Sep 29, 2016): I'll experiment for a while with that `assert()`. If it changes the current behavior of fetchExisting(), I might not put it in (example, if there are previous cases where `isDeleted` is marked but goes through `fetchExisting()` returning nil, adding an `assert()` will be a breaking change for those use cases)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/CoreStore#90