3

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;
}
10
  • __block NSError *blockError = nil; [@[@1] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { blockError = [NSError errorWithDomain:@"1" code:7 userInfo:@{}];}]; if (blockError) *error = blockError; return N0;? Commented Aug 28, 2017 at 16:10
  • @Larme Yeah, that's the question, is that always required? There's no way around that? If so, why? Commented Aug 28, 2017 at 16:11
  • 2
    FYI - Never do *error = ... without first making sure that error isn't nil. Never dereference a nil pointer. Commented Aug 28, 2017 at 16:12
  • If you write NSUInteger value = 0; before the enumerateObjectsUsingBlock: and then a try to set its value inside the block, the compiler should warn you (compile error or warning) asking for a __block. So I'd say it's the same here. Commented Aug 28, 2017 at 16:14
  • 2
    @rmaddy Technically, a check against an NSError ** pointer should be for NULL, not nil. nil is supposed to be used for object pointers (i.e. NSError *), not pointers to object pointers, although in practice they both have the same value. Commented Aug 28, 2017 at 19:00

1 Answer 1

10

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;
}
Sign up to request clarification or add additional context in comments.

2 Comments

I recommend the first approach. Using NSError * __strong * is very unconventional.
Apparently, this is documented, Values allocated within the block will be deallocated after the block is executed. Use retain to explicitly maintain those values. - developer.apple.com/documentation/foundation/nsarray/…

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.