0

How do I define a method that returns an error as well as a value?

For instance, when I call the managedObjectContext save method, the method returns a boolean, along with an error:

if(![context save:&error]) {
    NSLog(@"%@", error);
}

Would you be able to give me a straight-forward example of the method definition behind this?

Edit/Update: In the same regard, how would it be possible to pass back multiple errors. I'm doing something wrong below (I probably don't understand the concept yet), which doesn't work:

NSArray *errors = nil;
[self throwMultipleErrors:&errors];
for(id error in errors) {
    NSLog(@"Muliple error: %@", error);
}...

-(BOOL)throwMultipleErrors:(NSMutableArray **) errors {
    [*errors addObject:@"First Error"];
    [*errors addObject:@"Second Error"];
    [*errors addObject:@"Third Error"];

    return YES;
}
Allen
  • 3,601
  • 10
  • 40
  • 59
  • nevermind I forgot to instantiate the array by pointing it to nil :\ – Allen Oct 06 '13 at 22:46
  • Do not pass things around as `NSMutableArray**`. Use an `NSError**` and put your multiple sub-errors into the user info dictionary. – bbum Oct 06 '13 at 22:51

3 Answers3

4

In your example, the method signature is:

- (BOOL)save:(NSError **)error;

This allows the caller to pass in an NSError as an address rather than a pointer (pointer to a pointer, ** and &), that the method can then assign from another scope. And if the method returns false, the caller can then check the contents of the error variable, as shown in your example.

See this question for more information on using & in front of variables and (NSError **) signatures.

Why is `&` (ampersand) put in front of some method parameters?

Example implementation:

    - (BOOL)save:(NSError **)error
    {
       NSAssert(error != nil, @"Passed NSError must be nil!");
       // Attempt to do a save
       if (saveDidFail) {
             NSDictionary *userInfo = [NSDictionary dictionaryWithObjectAndKeys:@"Save Failed", kCustomErrorKey, nil];
             *error = [NSError errorWithDomain:@"domain" code:999 userInfo:userInfo];
             return NO;
       } 
       return YES;
    }
Community
  • 1
  • 1
Tim
  • 8,932
  • 4
  • 43
  • 64
  • can you show me how the how the error would be handled within the method? – Allen Oct 06 '13 at 22:10
  • Thanks Tim! Sorry just one more step ahead, I was wondering if it would be possible pass back an array of errors? I understand that it would require passing in something like -(BOOL)save:(NSMutableArray **) errors – Allen Oct 06 '13 at 22:30
  • 1
    You could do that, but I am not sure why you need to? Could you not pass back more information in the userInfo part of an NSError? But yes, (NSMutableArray **)errors, is what you'd do. – Tim Oct 06 '13 at 22:32
  • the reason why is because I'm making calls to multiple methods that pass back their own errors. for instance, i'm having a single function (registerUSer) which which encompasses saving to the database (which might return an error), as well as saving in core data (which might return a second error). I want registerUser to return an array of errors. – Allen Oct 06 '13 at 22:38
  • can you take a look at the updated question? what am i doing wrong? – Allen Oct 06 '13 at 22:40
  • 2
    Put the multiple errors into the user info dictionary; the 'kits do this in various places. Note that the above code is missing an `if(error)` before setting `*error*. – bbum Oct 06 '13 at 22:50
  • 1
    It is also missing a check that `*error` isn't already set, resulting in a potential memory management bug should the caller pass in a value with an object already assigned. – BergQuester Oct 07 '13 at 03:16
3

Using your example, the method signature would be:

-(BOOL)save:(NSError**)error;

Now, when using double pointers, you need to be defensive in your programming. First, you must make sure that the value of error is not nil, then *error is nil. You must do this because you don't know what to do memory management wise with an existing object. For example:

NSError *error = nil;
[self save:&error];

would be correct. But

NSError *error = [[NSError alloc] init];
[self save:&error];

is a problem because your save: method won't if error is retained or autoreleased. If you release it and it's autoreleased then your app will eventually crash. Conversely, if you don't release it and it is retained you leak memory. I recommend checking for this with an assert so that the problem is taken care of quickly before code is checked in.

NSAssert(!error || !*error, @"*error must be nil!");

Finally, when setting *error you must make sure that error is not nil. You are never permitted to set the value of memory address 0x0. If you don't check and if you pass nil into the method, you will crash if you try to set it.

if (error)
{
    *error = [NSError ...];
    return NO;
}

To return an array I would do something like this:

-(BOOL)throwMultipleErrors:(NSError **) error {

    NSAssert(!error | !*error, @"*error must be nil");

    NSMutableArray *errorList = nil;

    if (error)
        errorList = [NSMutableArray array];


    [errorList addObject:@"First Error"];
    [errorList addObject:@"Second Error"];
    [errorList addObject:@"Third Error"];

    if (error && [errorList count] > 0)
    {
        *error = [NSError errorWithDomain:@"Some error"
                                     code:0
                                 userInfo:@{@"suberrors" : [NSArray arrayWithArray:errorList]}];
        return NO;
    }

    return YES;
}

Note that the return array is immutable and is instantiated inside the method. There's really no reason for this array to be mutable outside of this method and for the array to be instantiated outside of it. Encapsulating the array in an NSError permits it to easily fit into an existing chain of NSError methods.

Notes

Of course, you would actually throw your [self save:&error]; call into an if() statement to check the return value. Do note, however, that we pass in &error rather than just error. This is because we need to pass in the pointer to error, not error itself. &error reads as "The address of error."

BergQuester
  • 6,167
  • 27
  • 39
  • thanks berquester! can you help me take a look at the updated question and tell me what I'm doing wrong when I want to pass back an array of errors? – Allen Oct 06 '13 at 22:41
  • 1
    Yes, I can, but first I have a question. Why do you need to return multiple errors? Why not bail out at the first error? That is normally how these things are handled. – BergQuester Oct 06 '13 at 22:43
  • nevermind, I forgot to instantiate the array. So basically I'm making calls to multiple methods that pass back their own errors. for instance, i'm having a single function (registerUSer) which which encompasses saving to the database (which might return an error), as well as saving in core data (which might return a second error). I want registerUser to return an array of errors. – Allen Oct 06 '13 at 22:47
  • 1
    Whipped up a defensive version of returning an array. – BergQuester Oct 06 '13 at 22:53
  • 1
    @BergQuester A better solution would be to have a generic encapsulating `NSError` whose user info dictionary contains a `suberrors` array. – bbum Oct 06 '13 at 23:35
2

We need to be careful with syntax here. When we pass an NSError into a method: + (instancetype)stringWithContentsOfFile:(NSString *)path encoding:(NSStringEncoding)enc error:(NSError * *)error

we hand in a handle (pointer to a pointer) to an error object which has not yet been initialized. Should the called message run into trouble it will initialize and fill the error object. It will NOT "return" an error, simply fill in values to a passed parameter.

so if I want to define a message that returns a value, and also takes an error parameter,Follow this tutorial: http://www.cimgf.com/2008/04/04/cocoa-tutorial-using-nserror-to-great-effect/

Hasan Edain
  • 104
  • 5