I would say, whether the API provides one completion handler or a pair of success/failure blocks, is primarily a matter of personal preference.
Both approaches have pros and cons, although there are only marginally differences.
Consider that there are also further variants, for example where the one completion handler may have only one parameter combining the eventual result or a potential error:
typedef void (^completion_t)(id result);
- (void) taskWithCompletion:(completion_t)completionHandler;
[self taskWithCompletion:^(id result){
if ([result isKindOfError:[NSError class]) {
NSLog(@"Error: %@", result);
}
else {
...
}
}];
The purpose of this signature is that a completion handler can be used generically in other APIs.
For example in Category for NSArray there is a method forEachApplyTask:completion:
which sequentially invokes a task for each object and breaks the loop IFF there was an error. Since this method is itself asynchronous as well, it has a completion handler as well:
typedef void (^completion_t)(id result);
typedef void (^task_t)(id input, completion_t);
- (void) forEachApplyTask:(task_t)task completion:(completion_t);
In fact, completion_t
as defined above is generic enough and sufficient to handle all scenarios.
However, there are other means for an asynchronous task to signal its completion notification to the call-site:
Promises
Promises, also called “Futures”, “Deferred” or “Delayed” represent the eventual result of an asynchronous task (see also: wiki Futures and promises).
Initially, a promise is in the “pending” state. That is, it’s “value” is not yet evaluated and not yet available.
In Objective-C, a Promise would be an ordinary object which will be returned from an asynchronous method as shown below:
- (Promise*) doSomethingAsync;
! The initial state of a Promise is “pending”.
Meanwhile, the asynchronous tasks starts to evaluate its result.
Note also, that there is no completion handler. Instead, the Promise will provide a more powerful means where the call-site can obtain the eventual result of the asynchronous task, which we will see soon.
The asynchronous task, which created the promise object, MUST eventually “resolve” its promise. That means, since a task may either succeed or fail, it MUST either “fulfill” a promise passing it the evaluated result, or it MUST “reject” the promise passing it an error indicating the reason for the failure.
! A task must eventually resolve its promise.
When a Promise has been resolved, it cannot change it's state anymore, including its value.
! A Promise can be resolved only once.
Once a promise has been resolved, a call-site can obtain the result (whether it failed or succeeded). How this is accomplished depends on whether the promise is implemented using the synchronous or the asynchronous style.
A Promise can be implemented in a synchronous or an asynchronous style which leads to either blocking respectively non-blocking semantics.
In a synchronous style in order to retrieve the value of the promise, a call-site would use a method which will block the current thread until after the promise has been resolved by the asynchronous task and the eventual result is available.
In an asynchronous style, the call-site would register callbacks or handler blocks which get called immediately after the promise has been resolved.
It turned out that the synchronous style has a number of significant disadvantages which effectively defeat the merits of asynchronous tasks. An interesting article about the currently flawed implementation of “futures” in the standard C++11 lib can be read here: Broken promises–C++0x futures.
How, in Objective-C, would a call-site obtain the result?
Well, it’s probably best to show a few examples. There are a couple of libraries which implement a Promise (see links below).
However, for the next code snippets, I will use a particular implementation of a Promise library, available on GitHub RXPromise. I’m the author of RXPromise.
The other implementations may have a similar API, but there can be small and possibly subtle differences in syntax. RXPromise is an Objective-C version of the Promise/A+ specification which defines an open standard for robust and interoperable implementations of promises in JavaScript.
All promise libraries listed below do implement the asynchronous style.
There are quite significant differences among the different implementations. RXPromise internally utilizes dispatch lib, is fully thread safe, extremely lightweight, and also provides a number of additional useful features, like cancellation.
A call-site obtains the eventual result of the asynchronous task through “registering” handlers. The “Promise/A+ specification” defines the method then
.
The method then
With RXPromise it looks as follows:
promise.then(successHandler, errorHandler);
where successHandler is a block which gets called when the promise has been “fulfilled” and errorHandler is a block which gets called when the promise has been “rejected”.
! then
is used to obtain the eventual result and to define a success or an error handler.
In RXPromise, the handler blocks have the following signature:
typedef id (^success_handler_t)(id result);
typedef id (^error_handler_t)(NSError* error);
The success_handler has a parameter result which is obviously the eventual result of the asynchronous task. Likewise, the error_handler has a parameter error which is the error reported by the asynchronous task when it failed.
Both blocks have a return value. What this return value is about, will become clear soon.
In RXPromise, then
is a property which returns a block. This block has two parameters, the success handler block and the error handler block. The handlers must be defined by the call-site.
! The handlers must be defined by the call-site.
So, the expression promise.then(success_handler, error_handler);
is a short form of
then_block_t block promise.then;
block(success_handler, error_handler);
We can write even more concise code:
doSomethingAsync
.then(^id(id result){
…
return @“OK”;
}, nil);
The code reads: “Execute doSomethingAsync, when it succeeds, then execute success handler”.
Here, the error handler is nil
which means, in case of an error, it will not be handled in this promise.
Another important fact is that calling the block returned from property then
will return a Promise:
! then(...)
returns a Promise
When calling the block returned from property then
, the “receiver” returns a new Promise, a child promise. The receiver becomes the parent promise.
RXPromise* rootPromise = asyncA();
RXPromise* childPromise = rootPromise.then(successHandler, nil);
assert(childPromise.parent == rootPromise);
What does that mean?
Well, due to this we can “chain” asynchronous tasks which effectively get executed sequentially.
Furthermore, the return value of either handler will become the “value” of the returned promise. So, if the task succeeds with the eventual result @“OK”, the returned promise will be “resolved” (that is “fulfilled”) with value @“OK”:
RXPromise* returnedPromise = asyncA().then(^id(id result){
return @"OK";
}, nil);
...
assert([[returnedPromise get] isEqualToString:@"OK"]);
Likewise, when the asynchronous task fails, the returned promise will be resolved (that is “rejected”) with an error.
RXPromise* returnedPromise = asyncA().then(nil, ^id(NSError* error){
return error;
});
...
assert([[returnedPromise get] isKindOfClass:[NSError class]]);
The handler may also return another promise. For example when that handler executes another asynchronous task. With this mechanism we can “chain” asynchronous tasks:
RXPromise* returnedPromise = asyncA().then(^id(id result){
return asyncB(result);
}, nil);
! The return value of a handler block becomes the value of the child promise.
If there is no child promise, the return value has no effect.
A more complex example:
Here, we execute asyncTaskA
, asyncTaskB
, asyncTaskC
and asyncTaskD
sequentially - and each subsequent task takes the result of the preceding task as input:
asyncTaskA()
.then(^id(id result){
return asyncTaskB(result);
}, nil)
.then(^id(id result){
return asyncTaskC(result);
}, nil)
.then(^id(id result){
return asyncTaskD(result);
}, nil)
.then(^id(id result){
// handle result
return nil;
}, nil);
Such a “chain” is also called “continuation”.
Error handling
Promises make it especially easy to handle errors. Errors will be “forwarded” from the parent to the child if there is no error handler defined in the parent promise. The error will be forwarded up the chain until a child handles it. Thus, having the above chain, we can implement error handling just by adding another “continuation” which deals with a potential error which may happen anywhere above:
asyncTaskA()
.then(^id(id result){
return asyncTaskB(result);
}, nil)
.then(^id(id result){
return asyncTaskC(result);
}, nil)
.then(^id(id result){
return asyncTaskD(result);
}, nil)
.then(^id(id result){
// handle result
return nil;
}, nil);
.then(nil, ^id(NSError*error) {
NSLog(@“”Error: %@“, error);
return nil;
});
This akin to the probably more familiar synchronous style with exception handling:
try {
id a = A();
id b = B(a);
id c = C(b);
id d = D(c);
// handle d
}
catch (NSError* error) {
NSLog(@“”Error: %@“, error);
}
Promises in general have other useful features:
For example, having a reference to a promise, via then
one can "register" as many handlers as desired. In RXPromise, registering handlers can occur at any time and from any thread since it is fully thread-safe.
RXPromise has a couple more useful functional features, not required by the Promise/A+ specification. One is "cancellation".
It turned out that “cancellation” is an invaluable and important feature. For example a call-site holding a reference to a promise can send it the cancel
message in order to indicate that it’s no longer interested in the eventual result.
Just imagine an asynchronous task which loads an image from the web and which shall be displayed in a view controller. If the user moves away from the current view controller, the developer may implement code which sends a cancel message to the imagePromise, which in turn triggers the error handler defined by the HTTP Request Operation where the request will be cancelled.
In RXPromise, a cancel message will only be forwarded from a parent to its children, but not vice versa. That is, a “root” promise will cancel all children promises. But a child promise will only cancel the “branch” where it is the parent. The cancel message will also be forwarded to children if a promise has been already resolved.
An asynchronous task can itself register handler for its own promise, and thus can detect when someone else cancelled it. It may then prematurely stop performing a possibly lengthy and costly task.
Here are a couple other implementations of Promises in Objective-C found on GitHub:
https://github.com/Schoonology/aplus-objc
https://github.com/affablebloke/deferred-objective-c
https://github.com/bww/FutureKit
https://github.com/jkubicek/JKPromises
https://github.com/Strilanc/ObjC-CollapsingFutures
https://github.com/b52/OMPromises
https://github.com/mproberts/objc-promise
https://github.com/klaaspieter/Promise
https://github.com/jameswomack/Promise
https://github.com/nilfs/promise-objc
https://github.com/mxcl/PromiseKit
https://github.com/apleshkov/promises-aplus
https://github.com/KptainO/Rebelle
and my own implementation: RXPromise.
This list is likely not complete!
When choosing a third library for your project, please check carefully if the implementation of the library follows the prerequisites listed below:
A reliable promise library SHALL be thread safe!
It’s all about asynchronous processing, and we want to utilize multiple CPUs and execute on different threads concurrently whenever possible. Be careful, most of the implementations are not thread safe!
Handlers SHALL be called asynchronously which respect to the call-site! Always, and no matter what!
Any decent implementation should also follow a very strict pattern when invoking the asynchronous functions. Many implementors tend to "optimize" the case, where a handler will be invoked synchronously when the promise is already resolved when the handler will registered. This can cause all sorts of issues. See Don't release Zalgo!.
There should also be a mechanism to cancel a promise.
The possibility to cancel an asynchronous task often becomes a requirement with high priority in the requirement analysis. If not, for sure there will be filed an enhancement request from a user some time later after the app has been released. The reason should be obvious: any task which may stall or take too long to finish, should be cancelable by the user or by a timeout. A decent promise library should support cancellation.