Artemiy
Artemiy

Reputation: 13

Correct way to pass NSError from URLSession

I have Network layer class, which has method with URL request. Seems like this:

- (void)networkRequestWithError:(NSError *__strong *)responseError
                  andCompletion:(void (^)(NSData*))completion
{
    NSURL *url = ...
                        
    NSURLSessionDataTask *dataTask = [NSURLSession.sharedSession
                                      dataTaskWithURL:url
                                      completionHandler:^(NSData *data,
                                                          NSURLResponse *response,
                                                          NSError *error) {
        // *responseError = error; for real errors
        *responseError = [NSError errorWithDomain:@"1"
                                             code:1
                                         userInfo:@{}];        
        completion(data);
    }];
    
    [dataTask resume];
}
@end

I create instance of this network layer in controller and I want to handle error in completion block:

    __block NSError *responseError; 
    
    [self.networkService networkRequestWithError:&responseError
                                  withCompletion:^(NSData*) {
                
        if (responseError != nil) {
            NSLog(@"%@",responseError.localizedDescription);
        } else {
            //Some action with data. No matter
        }
    }];

Problem: responseError has some value in dataTask completion scope (when I init it), but in my completion block in controller it always nil. I don't have any idea why.

Upvotes: 0

Views: 464

Answers (2)

newacct
newacct

Reputation: 122519

For a technical explanation of why your code didn't work, it has to with the fact that __block variables can be moved, and thus getting a pointer to it at one point doesn't mean that the pointer still points to the variable later. You have to be careful when taking the address of a __block variable.

As an optimization, blocks start out on the stack, and __block variable also start out in a special structure on the stack. When some code need to store the block for use after the current scope, it copies the block, which causes the block to be moved to the heap if it isn't already there (since things on the stack may not live beyond the current call). Moving a block to the heap also causes any __block variables that the block captures to be moved to the heap if it isn't already there (again, since it needs to be on the heap to outlive the current call).

Normally, when you get or set a __block variable, the compiler translates that into several pointer lookups behind the scenes to access the real location of the __block variable, so it transparently works, no matter if it's on the stack or heap. However, if you take the address of it to get a pointer, as in &responseError, you only get the address of it as the variable currently is, but it does not get updated if the variable is moved.

So what is happening in your case is that the __block variable responseError in your second piece of code starts out on the stack, and when you do &responseError, you get a pointer to its location on the stack. This pointer is passed into -networkRequestWithError:withCompletion:, and this pointer (confusingly also called responseError) is then captured into the block it creates and passes into -[NSURLSession dataTaskWithURL:completionHandler:].

At the same time, the __block variable responseError in your second piece of code is captured by the block you pass into -networkRequestWithError:withCompletion:, and this block (called completion) is then captured by the block you pass into -[NSURLSession dataTaskWithURL:completionHandler:]. That is an asynchronous operation, so they copy the block, which copies the first block, which moves the blocks, as well as the __block variable responseError, to the heap.

By the time the completion handler of -[NSURLSession dataTaskWithURL:completionHandler:] is called asynchronously, the stack frame where the original __block variable responseError was created has ended, and the pointer that we captured (called responseError in -networkRequestWithError:withCompletion:, which is a pointer to the original __block variable responseError on the stack) is a dangling pointer. Assigning to the thing pointed to by that pointer is undefined behavior, and may actually be overwriting some unrelated variables on the stack. On the other hand, the actual __block variable responseError on the heap is not changed, and still holds the value it originally held (nil).

In your second piece of code, if you printed out the address of responseError (&responseError) before the block and within the block, you will see that they are different, demonstrating that it was moved.

Upvotes: 0

matt
matt

Reputation: 535945

This is an asynchronous method. Don’t set the response error by indirection. Do exactly what dataTaskWithURL does: pass it as another parameter into the completion block.

 - (void)networkRequestWithCompletion:(void (^)(NSData*, NSError*))completion

Upvotes: 0

Related Questions