iosswiftcore-datatry-catchoutofrangeexception

How to fetch 0 or 1 entity with NSFetchRequest and fetchLimit = 1 from Core Data?


In a multi-language app I let users login via Sign In with Apple, Google or Facebook and then store their data (user id, given, last name, etc.) into UserEntity:

UserEntity

When creating an entity I set the stamp attribute to current epoch seconds.

Then in my custom view model I try to fetch at least one user with the following code and assign it to a @Published var, so that my app knows if the user has already logged in:

class UserViewModel: ObservableObject {
    @Published var myUser: UserEntity?

    init(language: String) {
        fetchMyUser(language: language)
    }
    
    func fetchMyUser(language:String) {
        print("fetchMyUser language=\(language)")
        guard let container = PersistenceController.shared[language]?.container else { return }
        let request = NSFetchRequest<UserEntity>(entityName: "UserEntity")
        request.sortDescriptors = [ NSSortDescriptor(keyPath: \UserEntity.stamp, ascending: false) ]
        request.fetchLimit = 1
        
        do {
            myUser = try container.viewContext.fetch(request)[0]
        } catch let error {
            print("fetchMyUser fetch error: \(error)")
        }
    }
}

Note: please ignore the language String parameter ("en", "de" or "ru"). It is being used to select the corresponding NSPersistentContainer. This code works and is not related to my question.

My problem is that the do/try/catch code fails with Fatal error: Index out of range when there are no entities in the container yet (the user has never logged in):

Exception

Shouldn't do/try/catch just catch this exception, why does my app crash and how to fix my code, so that 0 or 1 UserEntity is fetched and assigned to myUser?


Solution

  • The do/try/catch mechanism in Swift catches Errors which are thrown somewhere else, see Error Handling for the details. It is not a general exception handler mechanism.

    In particular, “index out of bounds” is a runtime error and cannot be caught. It will always terminate the program immediately.

    viewContext.fetch(request) throws an error if the fetch request could not be executed. Otherwise it returns an array or zero or more objects. In order to check if (at least) one object was returned, you have to check the count or isEmpty property of the return array.

    Another option (as mentioned in the comments), is to use the first property, which returns an Optional: either the first element (if the array is not empty), or nil. In your case that would be

    do {
        myUser = try container.viewContext.fetch(request).first
    } catch let error {
        print("fetchMyUser fetch error: \(error)")
    }