Search code examples
objective-ccocoaautomatic-ref-countingobjective-c-blocks

Call Method that assigns NSError in Block Crashes


I'd like to understand why this crashes with an EXC_BAD_ACCESS error. It returns from the method call fine, but then crashes immediately afterwards on the [self runMethodThatAssignsError:&error] .

I've found a similar post here, but it doesn't explain what is going on, and is rather old.

- (void)checkError {
    NSError *error;
    [self runMethodThatAssignsError:&error]; // crashes after returning
    NSLog(@"success");
}

- (BOOL)runMethodThatAssignsError:(NSError **)error {
    [@[@1] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        *error = [NSError errorWithDomain:@"1" code:7 userInfo:@{}];
    }];
    return NO;
}

Solution

  • Running your example code in Instruments, it appears that -[NSArray enumerateObjectsUsingBlock:] is wrapping its block in an autorelease pool. Since NSError ** pointers are, by default, implicitly assumed to be __autoreleasing, your NSError object is autoreleased when it is assigned to *error, and consequently reaped by -[NSArray enumerateObjectsUsingBlock:]'s autorelease pool.

    There are two ways to fix this. The first one is to use a local variable outside of the block, to cause ARC to retain it until after the enumeration has finished:

    - (BOOL)runMethodThatAssignsError:(NSError **)error {
        __block NSError *_error = nil;
    
        [@[@1] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
            _error = [NSError errorWithDomain:@"1" code:7 userInfo:@{}];
        }];
    
        if (error) *error = _error;
    
        return NO;
    }
    

    Alternatively, you can just declare the error parameter as __strong, which will prevent the NSError from being put in the autorelease pool in the first place. Note that this should only be done if the clients of this method are always going to be using ARC, because otherwise it will probably cause the errors to leak as the clients will not expect to have to release them, due to this approach being unconventional.

    - (BOOL)runMethodThatAssignsError:(NSError * __strong *)error {
        [@[@1] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
            if (error) *error = [NSError errorWithDomain:@"1" code:7 userInfo:@{}];
        }];
    
        return NO;
    }