This article is also available in Chinese.

Last week, I wrote about Promises, which are a high-level building blocks for dealing with asynchronous operations. Using just the fulfill(), reject(), and then(), functions, we can build up lots of functionality in a simple and composable way. I’d like to explore some of those here.

Promise.all

Probably the poster child for the value of promisifying all of your asynchronous callbacks is Promise.all. What this static function does is wait for all the promises you give it to fulfill, and once they have, Promises.all will fulfill itself with the array of all fulfilled values. For example, you might want to write code to hit an API endpoint once for each item in an array. map and Promise.all make that super easy:

let userPromises = users.map({ user in
	APIClient.followUser(user)
})
Promise.all(userPromises).then({
	//all the users are now followed!
}).onFailure({ error in
	//one of the API requests failed
})

To write Promise.all, we first need to create a new Promise the represents the state of all the promises combined. If the array is empty, we can fulfill immediately.

static func all<T>(promises: [Promise<T>]) -> Promise<[T]> {
    return Promise<[T]>(work: { fulfill, reject in
        guard !promises.isEmpty else { fulfill([]); return }
			
    })
}

Inside this promise, we need to loop through each promise and add a handler for success and one for failure. If any of the promises have failed, we can reject the larger promise.

for promise in promises {
    promise.then({ value in

    }).onFailure({ error in
        reject(error)
    })
}

Only when all of the promises are complete should it fulfill the larger promise. With a simple check to make sure none of the promises are rejected or pending and a little flatMap magic, we can fulfill the promise with the values of all the promises combined. The whole function together looks like this:

static func all<T>(promises: [Promise<T>]) -> Promise<[T]> {
    return Promise<[T]>(work: { fulfill, reject in
        guard !promises.isEmpty else { fulfill([]); return }
        for promise in promises {
            promise.then({ value in
                if !promises.contains({ $0.isRejected || $0.isPending }) {
                    fulfill(promises.flatMap({ $0.value }))
                }
            }).onFailure({ error in
                reject(error)
            })
        }
    })
}

Note that promises can only be fulfilled or rejected once. If fulfill or reject were called a second time, it wouldn’t have any effect on the state of the promise.

Promises, like NSOperations, are state machines, but they store all the important state representing their completion in a thread-safe way. This is a different approach than NSOperation, where you have to provide your own state, storage, and thread-safety mechanisms. Operations don’t store the resulting value, so you have to manage that yourself too.

NSOperation also holds data about threading models and priority order, whereas promises don’t make any guarantees about how the work will be completed, just that it will be completed. This is borne out by looking at the Promise class itself. Its only instance variables are the state, which holds information about whether it’s pending, fulfilled, or rejected (and the corresponding data), and an array of callbacks. (It also holds an isolation queue, but that’s not really state.)

delay

One useful promise is one that resolves itself after some delay.

static func delay(delay: NSTimeInterval) -> Promise<()> {
    return Promise<()>(work: { fulfill, reject in
        let nanoseconds = Int64(delay*Double(NSEC_PER_SEC))
        let time = dispatch_time(DISPATCH_TIME_NOW, nanoseconds)
        dispatch_after(time, dispatch_get_main_queue(), {
            fulfill(())
        })
    })
}

Internally, this could be implemented with usleep or some other method of delaying, but dispatch_after is simple enough for our usage. This promise will become more useful as we build up other interesting promises.

timeout

Next, we’ll use delay to build timeout. This promise will be rejected after a delay.

static func timeout<T>(timeout: NSTimeInterval) -> Promise<T> {
    return Promise<T>(work: { fulfill, reject in
        delay(timeout).then({ _ in
            reject(NSError(domain: "com.khanlou.Promise", code: -1111, userInfo: [ NSLocalizedDescriptionKey: "Timed out" ]))
         })
    })
}

This isn’t that useful of a promise by itself, but it will be useful in building a few of the other behaviors that we want.

race

The partner to Promise.all, which fulfills when all promises are fulfilled, is Promise.race, which fulfills or rejects with the first promise that completes.

static func race<T>(promises: [Promise<T>]) -> Promise<T> {
    return Promise<T>(work: { fulfill, reject in
        guard !promises.isEmpty else { fatalError() }
        for promise in promises {
            promise.then({ value in
                fulfill(value)
            }).onFailure({ error in
                reject(error)
            })
        }
    })
}

Because promises can only be fulfilled or rejected once, calling fulfill or reject on the outer promise after it’s already been moved out of the .Pending state won’t have any effect.

With this function, timeout, and Promise.race, we can now create a new promise that either succeeds, fails, or times out within a certain limit. We’ll make it an extension on Promise.

extension Promise {
	func addTimeout(timeout: NSTimeInterval) -> Promise<Value> {
	    return Promise.race([self, Promise.timeout(timeout)])
	}
}

It can be used within a normal promise chain, like so:

APIClient
    .getUsers()
    .addTimeout(0.5)
    .then({
    	//we got our users within 0.5 seconds
    })
    .onFailure({ error in
    	//maybe our timeout error, maybe a network error
    })

One of the reasons I like promises so much is that their composability lets us build up behaviors easily. Promises usually require a guarantee that they will be fulfilled or rejected at some point, but our timeout function allows us to correct that behavior in a general fashion.

recover

recover is another useful function. It lets us catch an error and easily recover from it without messing up the rest of the promise chain.

We know the form we want our function to take: it should accept a function that takes our error and returns a new promise. Our method will also return a promise that we can use to continue chaining off of.

extension Promise {
	func recover<T>(recovery: (ErrorType) -> Promise<T>) -> Promise<T> {
		
	}
}

For the body of the method, we know that we need to return a new promise, and if the current promise (self) succeeds, we should forward that success to the new promise.

func recover<T>(recovery: (ErrorType) -> Promise<T>) -> Promise<T> {
	return Promise(work: { fulfill, reject in
		self.then({ value in
			fulfill(value)
		}).onFailure({ error in

		}
	}
}

onFailure, however, is a different story. If the promise fails, we should call the recovery function that was provided. That will give us a new promise. If that recovery promise succeeds, we can pass on that success to the new promise, and same if it fails.

//..
}).onFailure({ error in
	recovery(error).then({ value in
		fulfill(value)
	}).onFailure({ error in 
		reject(error)
	}
})
//...

The completed function looks like this:

extension Promise {
    func recover(recovery: (ErrorType) -> Promise<Value>) -> Promise<Value> {
        return Promise(work: { fulfill, reject in
            self.then({ value in
                fulfill(value)
            }).onFailure({ error in
                recovery(error).then({ value in
                    fulfill(value)
                }).onFailure({ error in
                    reject(error)
                })
            })
        })
    }
}

With this new function, we can recover from errors. For example. if the network doesn’t load the data we expect to see, we can load the data from a cache:

APIClient.getUsers()
    .recover({ error in 
        return cache.getUsers()
    }).then({ user in
    	//update UI
    }).onFailure({ error in
    	//handle error
    })

retry

Retrying is another capability we can add. To retry, we need a number of times to retry and a function that will generate the promise to be retried (so we can create the promise multiple times).

static func retry<T>(count count: Int, delay: NSTimeInterval, generate: () -> Promise<T>) -> Promise<T> {
    if count <= 0 {
        return generate()
    }
    return Promise<T>(work: { fulfill, reject in
        generate().then({ value in
            fulfill(value)
        }).recover({ error in
            return self.delay(delay).then({
                retry(count: count-1, delay: delay, generate: generate)
            })
        }).onFailure({ error in
            reject(error)
        })
    })
}
  • If the count is 1 or less, just generate the promise and return it.
  • Otherwise, create a new promise which generates the promise, and recovers it with a delay followed by a retry of count-1.

Creating retry builds on both delay and recover, which we wrote above.

In each of these examples, small, composable pieces come together to make simple and elegant solutions. All of these behaviors are built on the simple .then and .onFailure functions provided by the core of the promise implementation. Simply by formalizing what completion blocks look like, we can solve problems like timeouts, recovery, and retrying, and we can do so in a simple and reusable way. These examples still need some tests and verification, but I’ll be adding them slowly to the GitHub repo in the coming days and weeks.