86

I've seen in the example code supplied by Apple references to how you should handle Core Data errors. I.e:

NSError *error = nil;
if (![context save:&error]) {
/*
 Replace this implementation with code to handle the error appropriately.

 abort() 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. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
 */
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
}

But never any examples of how you should implement it.

Does anyone have (or can point me in the direction of) some actual "production" code that illustrates the above method.

Thanks in advance, Matt

JamesENL
  • 6,028
  • 6
  • 38
  • 64
Sway
  • 1,587
  • 3
  • 15
  • 19

5 Answers5

33

No one is going to show you production code because it depends 100% on your application and where the error occurs.

Personally, I put an assert statement in there because 99.9% of the time this error is going to occur in development and when you fix it there it is highly unlikely you will see it in production.

After the assert I would present an alert to the user, let them know an unrecoverable error occurred and that the application is going to exit. You can also put a blurb in there asking them to contact the developer so that you can hopefully track this done.

After that I would leave the abort() in there as it will "crash" the app and generate a stack trace that you can hopefully use later to track down the issue.

Marcus S. Zarra
  • 46,143
  • 9
  • 99
  • 181
  • Marcus - While asserts are fine if you are talking to a local sqlite database or XML file, you need a more robust error handling mechanism if your persistent store is cloud based. – dar512 Apr 29 '13 at 19:52
  • 4
    If your iOS Core Data persistent store is cloud based, you have bigger problems. – Marcus S. Zarra Apr 29 '13 at 23:34
  • Hello @MarcusS.Zarra, here you say that you will use abort() function, but apple says not top use this function in shipping application. So what you have to say – Ranjit Aug 06 '14 at 09:25
  • 3
    I disagree with Apple on a number of topics. It is the difference between a teaching situation (Apple) and in the trenches (me). From an academic situation, yes you should remove aborts. In reality, they are useful to catch situations that you never imagined possible. Apple documentation writers like to pretend that every situation is accountable. 99.999% of them are. What do you do for the truly unexpected? I crash and generate a log so I can find out what happened. That is what abort is for. – Marcus S. Zarra Aug 06 '14 at 16:34
  • Although, sometimes a save: is not successful because of invalid data. If you use model validations, your app will crash due to something that *may* be easy to recover from. Adding in an abort can certain track this problem down, but in this particular case, you can also not crash and follow appropriate measures to correct the data so the save completes correctly. – casademora Aug 06 '14 at 17:53
  • @casademora that is a developer level issue. Inputting bad data is part of the testing of an application during development and will be caught before production release. That is in the category of "foreseeable problem". – Marcus S. Zarra Aug 06 '14 at 21:12
  • I disagree: A save can easily fail in production as well. If you are talking to a webservice and try to save retrieved objects there are numerous things that could go wrong while deserialization or re-establishing relationships client-side. Those errors are absolutely valid and need to be handled properly leaving a clean context behind. – cschuff Sep 03 '14 at 10:37
  • 1
    @cschuff, none of those impact a core data `-save:` call. All of those conditions happen long before your code would reach this point. – Marcus S. Zarra Sep 03 '14 at 15:57
  • @MarcusS.Zarra why is that? If I deserialize a NSDictionary from a webservice into a NSManagedObject and say it does not contain a field which is mandatory in my managedmodel. How is this not going to throw an error at save? That would just require a simple rollback to anticipate the erroneous webservice data instead of letting the whole app explode :/ – cschuff Sep 03 '14 at 20:04
  • 3
    That is an anticipated error that can be caught and corrected before the save. You can ask Core Data if the data is valid and correct it. Plus you can test that at time of consumption to make sure all valid fields are present. That is a developer level error that can be handled long before the `-save:` is called. – Marcus S. Zarra Sep 04 '14 at 07:24
32

This is one generic method I came up with to handle and display validation errors on the iPhone. But Marcus is right: You'd probably want to tweak the messages to be more user friendly. But this at least gives you a starting point to see what field didn't validate and why.

- (void)displayValidationError:(NSError *)anError {
    if (anError && [[anError domain] isEqualToString:@"NSCocoaErrorDomain"]) {
        NSArray *errors = nil;

        // multiple errors?
        if ([anError code] == NSValidationMultipleErrorsError) {
            errors = [[anError userInfo] objectForKey:NSDetailedErrorsKey];
        } else {
            errors = [NSArray arrayWithObject:anError];
        }

        if (errors && [errors count] > 0) {
            NSString *messages = @"Reason(s):\n";

            for (NSError * error in errors) {
                NSString *entityName = [[[[error userInfo] objectForKey:@"NSValidationErrorObject"] entity] name];
                NSString *attributeName = [[error userInfo] objectForKey:@"NSValidationErrorKey"];
                NSString *msg;
                switch ([error code]) {
                    case NSManagedObjectValidationError:
                        msg = @"Generic validation error.";
                        break;
                    case NSValidationMissingMandatoryPropertyError:
                        msg = [NSString stringWithFormat:@"The attribute '%@' mustn't be empty.", attributeName];
                        break;
                    case NSValidationRelationshipLacksMinimumCountError:  
                        msg = [NSString stringWithFormat:@"The relationship '%@' doesn't have enough entries.", attributeName];
                        break;
                    case NSValidationRelationshipExceedsMaximumCountError:
                        msg = [NSString stringWithFormat:@"The relationship '%@' has too many entries.", attributeName];
                        break;
                    case NSValidationRelationshipDeniedDeleteError:
                        msg = [NSString stringWithFormat:@"To delete, the relationship '%@' must be empty.", attributeName];
                        break;
                    case NSValidationNumberTooLargeError:                 
                        msg = [NSString stringWithFormat:@"The number of the attribute '%@' is too large.", attributeName];
                        break;
                    case NSValidationNumberTooSmallError:                 
                        msg = [NSString stringWithFormat:@"The number of the attribute '%@' is too small.", attributeName];
                        break;
                    case NSValidationDateTooLateError:                    
                        msg = [NSString stringWithFormat:@"The date of the attribute '%@' is too late.", attributeName];
                        break;
                    case NSValidationDateTooSoonError:                    
                        msg = [NSString stringWithFormat:@"The date of the attribute '%@' is too soon.", attributeName];
                        break;
                    case NSValidationInvalidDateError:                    
                        msg = [NSString stringWithFormat:@"The date of the attribute '%@' is invalid.", attributeName];
                        break;
                    case NSValidationStringTooLongError:      
                        msg = [NSString stringWithFormat:@"The text of the attribute '%@' is too long.", attributeName];
                        break;
                    case NSValidationStringTooShortError:                 
                        msg = [NSString stringWithFormat:@"The text of the attribute '%@' is too short.", attributeName];
                        break;
                    case NSValidationStringPatternMatchingError:          
                        msg = [NSString stringWithFormat:@"The text of the attribute '%@' doesn't match the required pattern.", attributeName];
                        break;
                    default:
                        msg = [NSString stringWithFormat:@"Unknown error (code %i).", [error code]];
                        break;
                }

                messages = [messages stringByAppendingFormat:@"%@%@%@\n", (entityName?:@""),(entityName?@": ":@""),msg];
            }
            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Validation Error" 
                                                            message:messages
                                                           delegate:nil 
                                                  cancelButtonTitle:nil otherButtonTitles:@"OK", nil];
            [alert show];
            [alert release];
        }
    }
}

Enjoy.

Johannes Fahrenkrug
  • 38,500
  • 17
  • 113
  • 155
  • 3
    Certainly cannot see anything wrong with this code. Looks solid. Personally I prefer to handle Core Data errors with an assertion. I have yet to see one make it to production so I have always considered them to be development errors rather than potential production errors. Although this is certainly another level of protection :) – Marcus S. Zarra Aug 18 '10 at 16:38
  • 2
    Marcus, about assertions: What is your opinion on keeping code DRY in terms of validations? In my opinion it is very desirable to define your validation criteria only once, in the model (where it belongs): This field can't be empty, that field has to be at least 5 chars long and that field has to match this regex. That _should_ be all the information needed to display an appropriate msg to the user. It somehow doesn't sit well with me to do those checks again in code before saving the MOC. What do you think? – Johannes Fahrenkrug Sep 07 '10 at 12:15
  • 2
    Never saw this comment since it wasn't on my answer. Even when you put validation in the model you still need to check to see if the object passed validation and present that to the user. Depending on design that could be at the field level (this password is bad, etc.) or at the save point. Designer's choice. I would not make that part of the app generic. – Marcus S. Zarra Jan 15 '13 at 18:54
  • 1
    @MarcusS.Zarra I guess you never got it because I didn't correctly @-mention you :) I think we fully agree: I'd like the validation-_information_ to be in the model, but the decision when to _trigger_ validation and how to handle and present the validation result should not be generic and should be handled in the appropriate places in the application code. – Johannes Fahrenkrug Jan 16 '13 at 07:53
  • The code looks great. My only question is, after showing the alert or logging the analysis, should I rollback the Core Data context or abort the app? Otherwise, I guess that the unsaved changes will keep causing the same problem when you try to save again. – Jake Jun 04 '13 at 18:17
  • @Jake Thanks! Well, the `NSValidationErrorObject` key will contain the object that didn't validate. When any object didn't validate, the save won't happen (the changed won't be written to disk). So depending on your app, you might want to prompt the user to fill in a valid value to fix the problem and then save again. – Johannes Fahrenkrug Jun 05 '13 at 08:23
  • @JohannesFahrenkrug Your advice makes sense. But, in my case, the data is not user-driven. It comes from a server during data synchronization. So, I decided to rollback the context if there's any error during saving. Thanks for you comment though. – Jake Jun 06 '13 at 16:58
  • it's nice to show an error message but the real problem is what to do about the context's invalid state. While it is in this invalid state no future saves work and worse they don't even have an error set. cschuff's answer begins to tackle this problem please see that. – malhal Dec 23 '15 at 11:01
6

I'm surprised no one here is actually handling the error the way it is meant to be handled. If you look at the documentation, you will see.

Typical reasons for an error here include: * The device is out of space. * The persistent store is not accessible, due to permissions or data protection when the device is locked. * The store could not be migrated to the current model version. * The parent directory does not exist, cannot be created, or disallows writing.

So if I find an error when setting up the core data stack, I swap the rootViewController of UIWindow and show UI that clearly tells the user that their device might be full, or their security settings are too high for this App to function. I also give them a 'try again' button, so they can attempt to fix the problem before the core data stack is re-attempted.

For instance the user could free up some storage space, return to my App and press the try again button.

Asserts? Really? Too many developers in the room!

I'm also surprised by the number of tutorials online that don't mention how a save operation could fail for these reasons also. So you will need to ensure that any save event ANYWHERE in your App could fail because the device JUST THIS MINUTE became full with your Apps saving saving saving.

  • This question is about the saving in the Core Data stack, it is not about setting up the Core Data Stack. But I agree its title could be misleading and maybe it should be modified. – valeCocoa Sep 13 '17 at 07:47
  • I disagree @valeCocoa. The post is clearly about how to handle save errors in production. Take another look. –  Sep 14 '17 at 08:35
  • @roddanash which is what I said… WtH! :) Take another look at your answer. – valeCocoa Sep 14 '17 at 13:16
  • You're crazy bro –  Sep 14 '17 at 19:09
  • you paste part of the documentation for the errors that can occur while instantiating the persistent store on a question regarding the errors occurring while saving the context, and I'm the crazy one? Ok… – valeCocoa Sep 15 '17 at 12:35
  • @valeCocoa who cares which part of the documentation it comes from. it's still truth. http://sendvid.com/d5u6ikmo –  Sep 16 '17 at 09:22
  • Never said it wasn't true, I said something different: that you were referring to the part of the documentation related to the persistent store initialization errors, and that the topic was about error handling when saving the context. And for that you decided to write that I'm crazy… I think we can stop here, at least you won't get any other answer from me, on my part the air has been cleared and I don't have the intention to follow a flame on Stack Overflow. – valeCocoa Sep 16 '17 at 15:48
  • crazy crazy crazy broooooo –  Sep 19 '17 at 06:51
  • thanks for a brilliant idea of presenting a new view controller. Haven't thought about it! – Andriy Gordiychuk Nov 02 '17 at 04:42
5

I found this common save function a much better solution:

- (BOOL)saveContext {
    NSError *error;
    if (![self.managedObjectContext save:&error]) {
        DDLogError(@"[%@::%@] Whoops, couldn't save managed object context due to errors. Rolling back. Error: %@\n\n", NSStringFromClass([self class]), NSStringFromSelector(_cmd), error);
        [self.managedObjectContext rollback];
        return NO;
    }
    return YES;
}

Whenever a save fails this will rollback your NSManagedObjectContext meaning it will reset all changes that have been performed in the context since the last save. So you have to watch out carefully to always persist changes using the above save function as early and regularly as possible since you might easily lose data otherwise.

For inserting data this might be a looser variant allowing other changes to live on:

- (BOOL)saveContext {
    NSError *error;
    if (![self.managedObjectContext save:&error]) {
        DDLogError(@"[%@::%@] Whoops, couldn't save. Removing erroneous object from context. Error: %@", NSStringFromClass([self class]), NSStringFromSelector(_cmd), object.objectId, error);
        [self.managedObjectContext deleteObject:object];
        return NO;
    }
    return YES;
}

Note: I am using CocoaLumberjack for logging here.

Any comment on how to improve this is more then welcome!

BR Chris

malhal
  • 17,500
  • 6
  • 94
  • 112
cschuff
  • 5,192
  • 3
  • 31
  • 51
  • I'm getting strange behaviour when I try to use rollback to achieve this: http://stackoverflow.com/questions/34426719/why-is-nsmanagedobjectcontextobjectsdidchangenotification-called-twice-with-the – malhal Dec 23 '15 at 01:36
  • I'm using undo instead now – malhal Sep 12 '16 at 20:55
2

I've made a Swift version of the useful answer of @JohannesFahrenkrug which can be useful :

public func displayValidationError(anError:NSError?) -> String {
    if anError != nil && anError!.domain.compare("NSCocoaErrorDomain") == .OrderedSame {
        var messages:String = "Reason(s):\n"
        var errors = [AnyObject]()
        if (anError!.code == NSValidationMultipleErrorsError) {
            errors = anError!.userInfo[NSDetailedErrorsKey] as! [AnyObject]
        } else {
            errors = [AnyObject]()
            errors.append(anError!)
        }
        if (errors.count > 0) {
            for error in errors {
                if (error as? NSError)!.userInfo.keys.contains("conflictList") {
                    messages =  messages.stringByAppendingString("Generic merge conflict. see details : \(error)")
                }
                else
                {
                    let entityName = "\(((error as? NSError)!.userInfo["NSValidationErrorObject"] as! NSManagedObject).entity.name)"
                    let attributeName = "\((error as? NSError)!.userInfo["NSValidationErrorKey"])"
                    var msg = ""
                    switch (error.code) {
                    case NSManagedObjectValidationError:
                        msg = "Generic validation error.";
                        break;
                    case NSValidationMissingMandatoryPropertyError:
                        msg = String(format:"The attribute '%@' mustn't be empty.", attributeName)
                        break;
                    case NSValidationRelationshipLacksMinimumCountError:
                        msg = String(format:"The relationship '%@' doesn't have enough entries.", attributeName)
                        break;
                    case NSValidationRelationshipExceedsMaximumCountError:
                        msg = String(format:"The relationship '%@' has too many entries.", attributeName)
                        break;
                    case NSValidationRelationshipDeniedDeleteError:
                        msg = String(format:"To delete, the relationship '%@' must be empty.", attributeName)
                        break;
                    case NSValidationNumberTooLargeError:
                        msg = String(format:"The number of the attribute '%@' is too large.", attributeName)
                        break;
                    case NSValidationNumberTooSmallError:
                        msg = String(format:"The number of the attribute '%@' is too small.", attributeName)
                        break;
                    case NSValidationDateTooLateError:
                        msg = String(format:"The date of the attribute '%@' is too late.", attributeName)
                        break;
                    case NSValidationDateTooSoonError:
                        msg = String(format:"The date of the attribute '%@' is too soon.", attributeName)
                        break;
                    case NSValidationInvalidDateError:
                        msg = String(format:"The date of the attribute '%@' is invalid.", attributeName)
                        break;
                    case NSValidationStringTooLongError:
                        msg = String(format:"The text of the attribute '%@' is too long.", attributeName)
                        break;
                    case NSValidationStringTooShortError:
                        msg = String(format:"The text of the attribute '%@' is too short.", attributeName)
                        break;
                    case NSValidationStringPatternMatchingError:
                        msg = String(format:"The text of the attribute '%@' doesn't match the required pattern.", attributeName)
                        break;
                    default:
                        msg = String(format:"Unknown error (code %i).", error.code) as String
                        break;
                    }

                    messages = messages.stringByAppendingString("\(entityName).\(attributeName):\(msg)\n")
                }
            }
        }
        return messages
    }
    return "no error"
}`
cdescours
  • 5,754
  • 3
  • 22
  • 30