Here is a typical promise chain:
firstly {
login()
}.then { creds in
fetch(avatar: creds.user)
}.done { image in
self.imageView = image
}
If this code used completion handlers, it would look like this:
login { creds, error in
if let creds = creds {
fetch(avatar: creds.user) { image, error in
if let image = image {
self.imageView = image
}
}
}
}
then
is just another way to structure completion handlers, but it is also quite a
bit more. At this initial stage of our understanding, it mostly helps
readability. The promise chain above is easy to scan and understand: one asynchronous
operation leads into the other, line by line. It's as close to
procedural code as we can easily come given the current state of Swift.
done
is the same as then
but you cannot return a promise. It is
typically the end of the “success” part of the chain. Above, you can see that we
receive the final image in our done
and use it to set up the UI.
Let’s compare the signatures of the two login methods:
func login() -> Promise<Creds>
// Compared with:
func login(completion: (Creds?, Error?) -> Void)
// ^^ ugh. Optionals. Double optionals.
The distinction is that with promises, your functions return promises instead
of accepting and running callbacks. Each handler in a chain returns a promise.
Promise
objects define the then
method, which waits for the completion of the
promise before continuing the chain. Chains resolve procedurally, one promise
at a time.
A Promise
represents the future value of an asynchronous task. It has a type
that represents the type of object it wraps. For example, in the example above,
login
is a function that returns a Promise
that will represent an instance
of Creds
.
Note:
done
was introduced in PromiseKit 5. We previously defined a variant ofthen
that did not require you to return a promise. Unfortunately, this convention often confused Swift and led to odd and hard-to-debug error messages. It also made using PromiseKit more painful. The introduction ofdone
lets you type out promise chains that compile without additional qualification to help the compiler figure out type information.
You may notice that unlike the completion pattern, the promise chain appears to ignore errors. This is not the case! In fact, it has the opposite effect: the promise chain makes error handling more accessible and makes errors harder to ignore.
With promises, errors cascade along the promise chain, ensuring that your apps are robust and your code is clear:
firstly {
login()
}.then { creds in
fetch(avatar: creds.user)
}.done { image in
self.imageView = image
}.catch {
// any errors in the whole chain land here
}
Swift emits a warning if you forget to
catch
a chain. But we'll talk about that in more detail later.
Each promise is an object that represents an individual, asynchronous task.
If a task fails, its promise becomes rejected. Chains that contain rejected
promises skip all subsequent then
s. Instead, the next catch
is executed.
(Strictly speaking, all subsequent catch
handlers are executed.)
For fun, let’s compare this pattern with its completion handler equivalent:
func handle(error: Error) {
//…
}
login { creds, error in
guard let creds = creds else { return handle(error: error!) }
fetch(avatar: creds.user) { image, error in
guard let image = image else { return handle(error: error!) }
self.imageView.image = image
}
}
The use of guard
and a consolidated error handler help, but the promise chain’s
readability speaks for itself.
We have learned to compose asynchronicity. Next let’s extend our primitives:
firstly {
UIApplication.shared.isNetworkActivityIndicatorVisible = true
return login()
}.then {
fetch(avatar: $0.user)
}.done {
self.imageView = $0
}.ensure {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
}.catch {
//…
}
No matter the outcome of your chain—-failure or success—-your ensure
handler is always called.
Let’s compare this pattern with its completion handler equivalent:
UIApplication.shared.isNetworkActivityIndicatorVisible = true
func handle(error: Error) {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
//…
}
login { creds, error in
guard let creds = creds else { return handle(error: error!) }
fetch(avatar: creds.user) { image, error in
guard let image = image else { return handle(error: error!) }
self.imageView.image = image
UIApplication.shared.isNetworkActivityIndicatorVisible = false
}
}
It would be very easy for someone to amend this code and forget to unset the activity indicator, leading to a bug. With promises, this type of error is almost impossible: the Swift compiler resists your supplementing the chain without using promises. You almost won’t need to review the pull requests.
Note: PromiseKit has perhaps capriciously switched between the names
always
andensure
for this function several times in the past. Sorry about this. We suck.
You can also use finally
as an ensure
that terminates the promise chain and does not return a value:
spinner(visible: true)
firstly {
foo()
}.done {
//…
}.catch {
//…
}.finally {
self.spinner(visible: false)
}
With completion handlers, reacting to multiple asynchronous operations is either slow or hard. Slow means doing it serially:
operation1 { result1 in
operation2 { result2 in
finish(result1, result2)
}
}
The fast (parallel) path code makes the code less clear:
var result1: …!
var result2: …!
let group = DispatchGroup()
group.enter()
group.enter()
operation1 {
result1 = $0
group.leave()
}
operation2 {
result2 = $0
group.leave()
}
group.notify(queue: .main) {
finish(result1, result2)
}
Promises are easier:
firstly {
when(fulfilled: operation1(), operation2())
}.done { result1, result2 in
//…
}
when
takes promises, waits for them to resolve and returns a promise containing the results.
As with any promise chain, if any of the component promises fail, the chain calls the next catch
.
When we made PromiseKit, we understood that we wanted to use only promises to implement asynchronous behavior. So wherever possible, we offer extensions to Apple’s APIs that reframe the API in terms of promises. For example:
firstly {
CLLocationManager.promise()
}.then { location in
CLGeocoder.reverseGeocode(location)
}.done { placemarks in
self.placemark.text = "\(placemarks.first)"
}
To use these extensions, you need to specify subspecs:
pod "PromiseKit"
pod "PromiseKit/CoreLocation"
pod "PromiseKit/MapKit"
To see what is available, check our sources.
The standard extensions will take you a long way, but sometimes you'll still need to start chains of your own. Maybe you're using a third party API that doesn’t provide promises, or perhaps you wrote your own asynchronous system. Either way, it's easy to add promises. If you look at the code of the standard extensions, you'll see that it uses the same approach described below.
Let’s say we have the following method:
func fetch(completion: (String?, Error?) -> Void)
How do we convert this to a promise? Well, it's easy:
func fetch() -> Promise<String> {
return Promise { fetch(completion: $0.resolve) }
}
You may find the expanded version more readable:
func fetch() -> Promise<String> {
return Promise { seal in
fetch { result, error in
seal.resolve(result, error)
}
}
}
The seal
object that the Promise
initializer provides to you defines
many methods for handling garden-variety completion handlers. It even
covers a variety of rarer situations, thus making it easy for you to add
promises to an existing codebase.
Note: We tried to make it so that you could just do
Promise(fetch)
, but we were not able to make this simpler pattern work universally without requiring extra disambiguation for the Swift compiler. Sorry; we tried.
Note: In PMK 4, this initializer provided two parameters to your closure:
fulfill
andreject
. PMK 5 and 6 give you an object that has bothfulfill
andreject
methods, but also many variants of the methodresolve
. You can typically just pass completion handler parameters toresolve
and let Swift figure out which variant to apply to your particular case (as shown in the example above).
Note:
Guarantee
s (below) have a slightly different initializer since they cannot error, so the parameter to the initializer closure is just a closure. Not aResolver
object. Just doseal(value)
rather thanseal.fulfill(value)
. It's different because there is only one way to seal guarantees; they can only fulfill.
Since PromiseKit 5, we have provided Guarantee
as a supplementary class to
Promise
. We do this to complement Swift’s strong error handling system.
Guarantees never fail, so they cannot be rejected. A good example is after
:
firstly {
after(seconds: 0.1)
}.done {
// there is no way to add a `catch` because after cannot fail.
}
Swift warns you if you don’t terminate a regular Promise
chain (i.e., not
a Guarantee
chain). You're expected to silence this warning by supplying
either a catch
or a return
. (In the latter case, you will then have to catch
at the point where you receive that promise.)
Use Guarantee
s wherever possible so that your code has error handling where
it's required and no error handling where it's not required.
In general, you should be able to use Guarantee
s and Promise
s interchangeably,
We have gone to great lengths to try and ensure this, so please open a ticket
if you find an issue.
If you are creating your own guarantees the syntax is simpler than that of promises:
func fetch() -> Promise<String> {
return Guarantee { seal in
fetch { result in
seal(result)
}
}
}
Which could be reduced to:
func fetch() -> Promise<String> {
return Guarantee(resolver: fetch)
}
then
provides you with the result of the previous promise and requires you to return
another promise.
map
provides you with the result of the previous promise and requires you to return
an object or value type.
compactMap
provides you with the result of the previous promise and requires you
to return an Optional
. If you return nil
, the chain fails with
PMKError.compactMap
.
Rationale: Before PromiseKit 4,
then
handled all these cases, and it was painful. We hoped the pain would disappear with new Swift versions. However, it has become clear that the various pain points are here to stay. In fact, we as library authors are expected to disambiguate at the naming level of our API. Therefore, we have split the three main kinds ofthen
intothen
,map
anddone
. After using these new functions, we realized this is much nicer in practice, so we addedcompactMap
as well (modeled onOptional.compactMap
).
compactMap
facilitates quick composition of promise chains. For example:
firstly {
URLSession.shared.dataTask(.promise, with: rq)
}.compactMap {
try JSONSerialization.jsonObject($0.data) as? [String]
}.done { arrayOfStrings in
//…
}.catch { error in
// Foundation.JSONError if JSON was badly formed
// PMKError.compactMap if JSON was of different type
}
Tip: We also provide most of the functional methods you would expect for sequences, e.g.,
map
,thenMap
,compactMapValues
,firstValue
, etc.
We provide get
as a done
that returns the value fed to get
.
firstly {
foo()
}.get { foo in
//…
}.done { foo in
// same foo!
}
We provide tap
for debugging. It's the same as get
but provides the
Result<T>
of the Promise
so you can inspect the value of the chain at this
point without causing any side effects:
firstly {
foo()
}.tap {
print($0)
}.done {
//…
}.catch {
//…
}
We've used firstly
several times on this page, but what is it, really? In fact,
it is just syntactic sugar.
You don’t really need it, but it helps to make your chains more readable. Instead of:
firstly {
login()
}.then { creds in
//…
}
You could just do:
login().then { creds in
//…
}
Here is a key understanding: login()
returns a Promise
, and all Promise
s have a then
function. firstly
returns a Promise
, and then
returns a Promise
, too! But don’t worry too much about these details. Learn the patterns to start with. Then, when you are ready to advance, learn the underlying architecture.
when
is one of PromiseKit’s more useful functions, and so we offer several variants.
-
The default
when
, and the one you should typically use, iswhen(fulfilled:)
. This variant waits on all its component promises, but if any fail,when
fails too, and thus the chain rejects. It's important to note that all promises in thewhen
continue. Promises have no control over the tasks they represent. Promises are just wrappers around tasks. -
when(resolved:)
waits even if one or more of its component promises fails. The value produced by this variant ofwhen
is an array ofResult<T>
. Consequently, this variant requires all its component promises to have the same generic type. See our advanced patterns guide for work-arounds for this limitation. -
The
race
variant lets you race several promises. Whichever finishes first is the result. See the advanced patterns guide for typical usage.
Swift automatically infers returns and return types for one-line closures. The following two forms are the same:
foo.then {
bar($0)
}
// is the same as:
foo.then { baz -> Promise<String> in
return bar(baz)
}
Our documentation often omits the return
for clarity.
However, this shorthand is both a blessing and a curse. You may find that the Swift compiler often fails to infer return types properly. See our Troubleshooting Guide if you require further assistance.
By adding
done
to PromiseKit 5, we were able to blunt many of these common pain points in using PromiseKit and Swift.
The above information is the 90% you will use. We strongly suggest reading the API Reference. There are numerous little functions that may be useful to you, and the documentation for everything outlined above is more thorough at the source.
In Xcode, don’t forget to option-click on PromiseKit functions to access this documentation while you're coding.
Here are some recent articles that document PromiseKit 5+:
Be careful when consulting general online references, as many of them refer to PMK < 5, which has a subtly different API. (Sorry about that, but Swift has changed a lot over the years and thus we had to as well.)