Promises Cookbook
Recipe 0: Understand return values
The key to understanding promises is to understand how return values move through the promise resolution process. Each of the recipes, below, illustrates a different aspect of this.
A promise is essentially a function pointer. You can pass the function pointer around, but you don't get the value out until you call it.
The way to call a promise is to then
it. However, because the function is asynchronous, it can't pass its return value to the left. Instead, it passes it into its then
block.
Recipe 1: Get the value from a promise
// Method signature
// Promise resolves to User instance
- (PMKPromise *)loginToRemoteServer:(NSString *)userId;
// Getting the value out.
[self loginToRemoteServer:userId].then(^(User *user) {
self.user = user;
});
Notes
- We always get the value(s) out of promises via side effects.
- It's good practice to document the return type of promise-returning methods, since the method signature no longer does so.
Recipe 2: Handle errors
Every promise actually has the potential to return two different types: its "promised" type and NSError
. If the promise fails, the then
block is skipped.
[self loginToRemoteServer:userId].then(^(User *user) {
self.user = user;
}).catch(^(NSError *error){
// Something went wrong; deal with it here
...
return nil;
});
Notes
- Return anything other than
NSError
to stop the error from bubbling up.
Recipe 3: Create a sequence of promises
A single promise is basically a fancy callback with a catch
block. The power of promises comes from the fact that they are chainable (aka composable).
First, look at the unchained (ie., nested) version. It is very similar to nested callbacks and just as with callbacks, if we added error handling to each promise, it would start to get messy.
// Method signatures
// Promise resolves to User instance
- (PMKPromise *)loginToRemoteServer:(NSString *)userId;
// Promise resolves to NSDictionary of user data
- (PMKPromise *)retrieveUserData:(User *)user;
// Promise resolves to UIImage
- (PMKPromise *)retrieveProfileImage:(NSURL *)imageURL;
[self loginToRemoteServer:userId].then(^(User *user) {
self.user = user;
[self retrieveUserData:user].then(^(NSDictionary *userData){
self.user.data = userData;
[self retrieveProfileImage:userData.profileImageURL].then(^(UIIMage *userImage){
self.user.profileImage = userImage;
});
});
})
Here is the chained version. Note the single catch block that simplifies error handling.
[self loginToRemoteServer:userId].then(^(User *user) {
self.user = user;
return [self retrieveUserData:user];
}).then(^(NSDictionary *userData){
self.user.data = userData;
return [self retrieveProfileImage:userData.profileImageURL];
}).then(^(UIImage *userImage){
self.user.profileImage = userImage;
}).catch(^(NSError *){
// Catch any errors here
...
});
Notes: Rules for chained return values
Notice that in the chained version, each of the then
blocks has a return value. Here's how it works:
- A
then
block's non-error return value gets passed to the nextthen
block; - If a
then
block returnsNSError
, that value gets passed to the nextcatch
block (skipping interveningthen's
); - If a
then
block doesn't return a value, the nextthen
is called as soon as the block completes.
And here's the most important part:
- If a
then
block returns a promise, the nextthen
receives its resolved value. This is the magic that lets us write asynchronous code almost as if it's synchronous.
Recipe 4: Branching
myURLPromise.then(^id(NSURL *url){
if (url) {
return url; // Got my url, I'm done, return the value
}
else {
return myOtherURLPromise; // Not done, do some more work
}
}).then(^(NSURL *url){
// Do something with URL ...
});
Notes
Until now, the method signatures of our then
blocks have all omitted their return types. These return types are being filled in via introspection. But whenever a block returns more than one type (as may happen when branching), the return type must be explicitly typed to id
.
In this example, the first return value is NSURL *
but the second is PMKPromise *
. The compiler won't accept that unless we specify the return type of the block as id
.
Recipe 5: Looping
You want to get the results from a set of promises whose number is not known in advance. Use the when
class method to handle multiple promises at the same time.
// Promise resolves to array of UIImages
- (PMKPromise *)getImagesForURLs:(NSArray *)urls
{
NSMutableArray *promises = [NSMutableArray array];
for (NSURL *url in urls) {
// getImageForURL returns promise that resolves to UIImage
[promises addObject:[self getImageForURL:url]];
}
return [PMKPromise when:promises].then(^(NSArray *results){
NSMutableArray *images = [NSMutableArray array];
for (UIImage *image in results) {
[images addObject:image];
}
return images;
});
}
then
block.
Recipe 6: Pass primitives to You can return primitives from a then
block, but the arguments passed into the next block are always boxed (ie., wrapped as objects).
myPromise.then(^{
...
return 3;
}).then(^(NSNumber *result){
NSInteger intResult = [result integerValue];
...
});
Recipe 7: Wrap an asynchronous method inside a promise
Any method that takes a callback (block) can be turned into a promise by wrapping it. Here, we're wrapping Firebase's oauth method.
// Promise resolves to FAuthData.
- (PMKPromise *)firebaseAuthWithOAuthProvider:provider accessToken(NSString *)token
{
PMKPromise *promise = [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
[firebase authWithOAuthProvider:provider token:token withCompletionBlock:^(NSError *error, FAuthData *authData) {
if (error) {
reject(error);
}
else {
fulfill(authData);
}
}];
}];
return promise;
}
Recipe 8: Use promises as building blocks
// Promise returns User instance
- (PMKPromise *)setupUserWithId:(NSString *)userId
{
PMKPromise *promise;
if (self.user) {
promise = [PMKPromise promiseWithValue:self.user]; // Resolves immediately
}
else {
promise = [self loginToRemoteServer:userId];
}
return promise.then(^(User *user) {
self.user = user;
return [self retrieveUserData:user];
}).then(^(NSDictionary *userData){
self.user.data = userData;
return [self retrieveProfileImage:userData.profileImageURL];
}).then(^(UIImage *userImage){
self.user.profileImage = userImage;
return self.user; // Return set up User as promise fulfillment
}).catch(^(NSError *){
// Log error but re-throw so caller can handle it
// If we returned nil here, error would be considered handled
NSLog(@"Error in setupUserWithId: %@", error);
return error;
});
}
... elsewhere ...
[self setupUserWithId:userId].then(^(User *user){
// User is set up; do something with it
}).catch(^(NSError *error){
// Let app user know that there's a problem
});