How to handle errors in NSPersistentContainer.loadPersistentStores? - ios

My AppDelegate came with the following all familiar Core Data template:
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "newsapp")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
It also had the following comment:
The persistent container for the application. This implementation
creates and returns a container, having loaded the store for the
application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
Replace this implementation with code to handle the error appropriately.
fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
Typical reasons for an error here include:
The parent directory does not exist, cannot be created, or disallows writing.
The persistent store is not accessible, due to permissions or data protection when the device is locked.
The device is out of space.
The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
True, calling it quits in this place is a bad idea not only it is not allowed in production apps, but because, as I read elsewhere, if a data store gets corrupted for any reason, a user would have to reinstall the app not knowing about that. A user can delete and forget my app just as well in this case.
Now, how one is supposed to handle such and other errors here? Even if I write the error handling code, how do I test that is works correctly if these errors almost never happen?
I looked all over, but couldn't find any example.

In most cases fatalError is the only "handling" that makes sense for this error, though you might want to display an alert telling the user what's happening first. If you need your persistent store but you can't load it, you're kind of screwed.
Some of those errors are things that should come up during development. Like store migration or data protection problems-- you need to be testing that, and if necessary fixing the app before release. For cases like that, fatalError actually makes sense because only you will ever experience it.
Of those listed examples, the only one that might come up unexpectedly that can be awkward to test is the device being out of space. You could check available space and alert the user. But you still don't have any way to recover from the error unless your app is using a lot of space that you can clear out. If this happens, iOS will already be warning them that they're running out of space, so doing it yourself isn't necessary.
If you don't have a lot of data you can clear out, fatalError still makes sense here.

Related

What to do about potential error 'throws' before submitting app to app store?

Lets say we have an app that we are about to submit to the app store... but it contains the following code:
try? audioSession.setCategory(AVAudioSessionCategoryAmbient)
if error != nil {
// this should never happen
}
try? audioSession.setActive(true)
if error != nil {
// this should never happen
}
We have to "try" the setCategory method because it "throws" ... but I have no idea why it would ever throw/fail.
I feel like this will be viewed as trash code... and that I should be doing more than just saying "this should never happen," but if an error did happen, I would have no idea what to do with it.
This documentation doesn't give any reasons why there would be an error, either.
Am I supposed to make a pop-up window that says "sorry... we encountered an error while attempting to set the category on the audioSession singleton instance... but the reason for this error is beyond this app's control. Press OK so the app can close. This never happened during app testing... so I have no idea why it happened to you now... must have something to do with your device. Maybe reboot? Better luck next time."
Or am I supposed to "elegantly handle the error?" How can I do that when I don't know what the potential error "throws" are and more importantly the potential reasons behind them.
Thanks for any suggestions. :)
I see two strategies that you could adopt.
Assume that the only way to throw an error is to pass in an invalid category string. Since your string is hard coded to one of the supplied constants, you can assume this will never happen. In this case, make the try cause a fatal error if it throws. e.g.
try! audioSession.setCategory(AVAudioSessionCategoryAmbient)
or
do
{
try audioSession.setCategory(AVAudioSessionCategoryAmbient)
}
catch
{
fatalError("setCategory failed with error: \(error)")
}
Assume there may be some local reason why it hasn't worked. In this case, use a do ... catch to catch the error and report it to the user. This doesn't have to happen in the function that calls audioSession.setCategory, you can choose to allow it to throw and allow other functions in the call stack to throw until you get to a point where you can easily report the error. If you get to this point, make sure the intermediate throwing functions are capable of clearing up their state by using defer and catch blocks.
If there is even a remote chance this might happen, and even though you have no proper way to handle it (let us say that the app is built upon that feature and if it is not working then the app becomes useless) - even then I would recommend putting there some nice handling code - I believe it's better than just let the app crash.
I would probably ask the user to report this to you with some details on their setup (after all, the error itself should contain at least a message that might give you a hint what went wrong).
If the app crashes, I would just assume the developer made some mistake. If a popup tries to at least broadly explain that there were some problems with obtaining an audio session, I would at least know that the developer tried to do something about it.
Basically, while you cannot do anything about recovering from the error, you can do at least something about user experience.
If you are sure errors will never occur you can force try
try! audioSession.setCategory(AVAudioSessionCategoryAmbient)
try! audioSession.setActive(true)

How should you handle errors in a production environment?

I've been following along with some tutorials and have built an app that I'd like to publish, but I'm unsure how to deal with error handling.
Tutorials teach you about things like do-catch blocks but whenever they deal with the case where an error is thrown, they just log the error to console, which is obviously unhelpful in a production environment.
Moreover, some examples of the do-catch blocks appear to have little use. As an example, I'm using Realm Database as a core part of my app, and I'm told the way to initialise it is like so in the AppDelegate:
do {
_ = try Realm()
} catch {
// TODO: - Handle Exception
}
But in this case, since realm is a core part of my app, I wouldn't even mind if it crashed the app, because if it doesn't init properly it's going to crash later anyway.
There are obvious cases when I should just display an error to the user (e.g. If you're trying to connect to an API but there is no wifi), or where a default value could be provided, but I don't know what to do here.
There's no single good approach to handling errors, since how an error should be handled completely depends on the exact situation.
In general, however, there are some guidelines on error handling:
Use do-catch blocks on throwable functions unless you have a very
good reason to do so
Avoid runtime errors at all costs and don't throw fatal errors. Even if your app encounters an error that cannot be properly handled (the results of a throwable function is absolutely needed for your app to function), don't let your app crash due to a runtime error, rather let the user know that there's something wrong. A crashing application sends a very bad message to your users, while failing gracefully after encountering an error doesn't seem as bad.
In a catch block, try to solve the error without
letting the user know that there was an error if the error is
recoverable (for instance, you can use a default value instead of the
value you expected from the throwable function)
If the error can't be
handled, let the user know that there was an error (such as they
don't have internet connection when making a network request or a
network request failed for any other reason) and try to give them an
alternative approach (i.e. try later when you have internet
connection)
Only use try! and similar forced methods (force unwrapping, etc.) if you are absolutely sure that the function won't actually throw an error (for instance Realm.init() can only throw an error on the very first call to it in an app's lifecycle, so after the first instantiation of Realm, you can safely do try! Realm()) or if the error represent a programmer error (such as a file not being in the right place, which is needed for the application), but make sure you actually correct such errors in the development phase

Definitive list of possible reasons NSManagedObjectContext save might fail

TL;DR version: is there a definitive list of possible reasons that -[NSManagedObjectContext save:] could fail? I'm looking in CoreDataErrors.h, but it's not clear which of the keys relate to NSDetailedErrorsKey.
Background: I'm maintaining a shipping app written in a combination of Swift and Objective-C. It uses JSQCoreDataKit. Someone else wrote the code originally.
We have a production crash that we have not been able to reproduce. We can tell from the stacktrace that it is crashing in an error handler following an NSManagedObjectContext save failure with EXC_BAD_ACCESS trying to log the NSError to the console. The save failure is on a child context and we know the model class which was being modified at the time.
Hence we can tell that the save is failing, but we have no detailed information about the reason for the save failure.
We want to work backwards from the reason for the save failure, in the hopes of working out a reproduction for the crash, in order to test any potential fixes.
The crash is actually a side effect of the save failure. So although we want to fix the crash, we also need to work out the reason for the save failure.
For 95% of users, the save happens without problems.
The model has some non-optional fields and the data comes from JSON parsed from a server response. At this stage we have no reason to suspect the server is sending us bad data.
Does anyone know of a definitive list of possible reasons for a save failure that we could work through, eliminating options?
So far, I'm aware of:
validation failure e.g. a missing a required value, or a value outside specified max/min values in the model.
a possible save conflict (see NSPersistentStoreSaveConflictsErrorKey). But it's not clear if it's possible to have this when saving from a child context to a parent context.
There should never be a failure -save: with no error. Every time I have seen that situation it was my fault in my code somewhere.
Can you update the question with showing the code around the error location?
The possible failures of a save are:
Merge failure (most common)
Validation failure (less common)
No store defined in the NSPersistentStore
A nil NSManagedObjectContext (this would present the case you describe)
All of those except for the last will produce an error object to interrogate.

iOS persistent cache directory

My app downloads data from the internet and then stores it somewhere on the device, so when the user has no internet connection the data will still be available. I know about the cache folder, but according to Apple this directory will be removed if the device runs out of disk space. This is really a problem for my app, since this might break the app for users with low disk space and a bad internet connection.
My question: Where do I save those files without the risk of them being deleted by the system due to low disk space or whatever. I can't place it in the documents directory because Apple will reject my application.
I have tried storing it in the documents folder, and then using the following line of code to disable iCloud backups.
try? NSURL.fileURLWithPath(self.path).setResourceValue(true, forKey: NSURLIsExcludedFromBackupKey)
But that doesn't work at all, it returns "()".
From the doc:
HANDLING ERRORS IN SWIFT: In Swift, this method returns Void and is
marked with the throws keyword to indicate that it throws an error in
cases of failure.
So () is the expected result. The real test of "working" is whether the file contents are retrievable and whether they don't synch to the cloud.
As an aside, I like better your original idea to use the NSCaches directory. It is exactly for content that can be regained with a fetch, but too expensive to fetch all the time. And having it emptied to keep the device running well sounds like exactly the right behavior.
The requirement that you've given yourself to deliver rich content, have a low memory impact on the device and work well with little or no network is a commendably high bar... too high in my opinion, compared to anyone else.
It seems to me reasonable to detect a no cache, low memory, disconnected situation and just apologize to the user. Tell them its their fault.

Best Way To Fix Core Data Migration Error

I have an application which uses core data and I have set up lightweight migration. When you first create the app xcode generates default methods. The persistentStoreCoordinator getter calls the abort() method and it says it should be replaced in a shipping application. I think the best way for my app to handle this failed migration would be to completely reset core data and then reload all the info. Is this the best method and if so how would I go about doing this?
The "best" answer always depends on your application.
This error should only ever happen during development. With proper testing, you should never see this error in production. I always consider it a development level error and therefore a NSLog and abort() are appropriate.
If your data is replaceable (i.e. you are using CD to cache data from the internet, etc.) then yes it makes sense to delete the database on this error and rebuild it from scratch. If your data is not replaceable then this is a terrible solution, see point 1.
Update 1
Putting this in an update because it is important and I don't want it getting lost in the comments:
#MarcusS.Zarra I know that this error should never happen, however I don't think it is unrecoverable.
In production, if you error on -addPersistentStore... it is unrecoverable as far as that database otherwise you would not have hit the error in the first place. Hence the two suggestions above, test more or replace the data.
There are two errors (ignoring iCloud) that you will get from that method, failed migration or failed to open the file.
If you failed to open the file the file is dead and is not going to be opened without human intervention.
If you failed the migration then you are missing the source data model and you are not going to recover it without a new build of the application that includes the source model.
Having a bad data file is extraordinarily rare. It indicates a fault with the file system or some other extremely unusual circumstance.
Not having the source data model is a 100% developer avoidable situation.
In either case, in production, you are not recovering from the error.
For development purposes I've previously added code to delete the database file before abort() is called.. That way when you relaunch the app, the database is reconfigured automatically to save you having to manually delete the app

Resources