Promises and ObjectiveC: no more callback hell

If you worked with asynchronous tasks in iOS apps, chances are that you have run into problems when implementing chains of async operations, like when it comes to talking to a server API.

Let’s take an example. Supposing you have to implement a library app and you need to add support for inserting a new book. And supposing the book name validation needs to be done on the server side. And to complicate things even further, the add book API returns only the id of the inserted book. The rest of the details will need to be fetched by making another call to the server. Basically the flow will be as following:

  1. ask the server if the book name is valid
  2. add the book
  3. fetch the book details

How would the ObjectiveC code look like to accomplish this flow? Assuming you’re using AFNetworking directly (although I personally would implement a layer of abstractization over it), one could write it like this:
[objc gutter=”false”]
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
NSDictionary *bookData = @{@"name": @"Harry Potter"};
[manager POST:@"http://myserver.com/validator/book"
parameters:bookData
success:^(AFHTTPRequestOperation *operation, id responseObject) {
[manager POST:@"http://myserver.com/book"
parameters:bookData
success:^(AFHTTPRequestOperation *operation, id responseObject) {
[manager GET:[@"http://myserver.com/book/" stringByAppendingString:responseObject]
parameters:nil
success:^(AFHTTPRequestOperation *operation, id responseObject) {
// inform the user that the book was added, move to the appropriate screen
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if([error.domain isEqual:NSURLErrorDomain]) {
// inform the user that there was a server communication problem
} else {
// inform the user that the book could not be added (e.g. it was already added by someone else)
}
}];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if([error.domain isEqual:NSURLErrorDomain]) {
// inform the user that there was a server communication problem
} else {
// inform the user that the book was added, however could not fetch its data
}
}];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if([error.domain isEqual:NSURLErrorDomain]) {
// inform the user that there was a server communication problem
} else {
// inform the user that the validation failed (i.e. the book data is not valid)
}
}];
[/objc]
Well, the above code has some issues. It has at least three levels of indentation, the failure block has redundant code (although the quantity of redundant code can be decreased), and let’s face it, chaining asynchronous operations this way is tedious and is not very maintainable. And it suffers the unwanted result of moving the code more and more to the right. This problem is very suggestively named “callback hell”.

Another disadvantage of the above is the fact that the final book data, which in our case is a NSDictionary, will need to be casted if we’ll want to access some NSDictionary specific properties. Not having a clear indication in the completion handler about the actual data type makes the code a little ambiguous.

The good news is that there is an alternative to this approach, an alternative that allows you to write code in a sync-ish manner, while not losing the advantages of async execution: promises. Here’s how the add book code would look like if AFHTTPRequestOperation would have a “promise” method that returns a promise:
[objc gutter=”false”]
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
NSDictionary *bookData = @{@"name": @"Harry Potter"};
[manager POST:@"http://myserver.com/validator/book"
parameters:bookData].promise.done(^{
return [manager POST:@"http://myserver.com/book"
parameters:bookData].promise;

}).success(^(NSString *bookId){
return [manager GET:[@"http://myserver.com/book/" stringByAppendingString:bookId]
parameters:nil].promise;

}).success(^(NSDictionary *bookDetails){
// inform the user that the book was added, move to the appropriate screen

}).failure(^(NSError *error) {
if([error.domain isEqual:NSURLErrorDomain]) {
// inform the user that there was a server communication problem
} else {
// inform the user about the problem
// error.domain/error.code can be used to identify which one of the
// three operations failed
}
});
[/objc]
The code is cleaner, it’s more readable, it closely follows the synchronous approach, thus the flow is more clear.

If I made you curious, you can read the promise specs here, and if you’re searching for an ObjectiveC implementation, you can find one here.

For now, that’s all about promises. There are much more things to discuss on this, and on the ObjectiveC implementation I mentioned above. I will try to cover those topics in the next articles, meanwhile if you have any questions feel free to add them as comments.

Update: You can check my next article on promises here: https://blog.cristik.com/2015/03/promises-basics/.

Leave a Reply

Your email address will not be published. Required fields are marked *