I know what you’re thinking, PromiseKit 6, whatever happened to PromiseKit 5?
Well PromiseKit 6 is almost identical to 5, but it had a minor, but breaking change and we respect semantic versioning.
Anyway, we’ll talk about that soon.
Guarantee<T>
Some asynchronous tasks cannot error, to represent this we now have Guarantee
.
Rather than return a Promise
, if an asynchronous task cannot error it returns
a Guarantee
:
after(seconds: 0.3).then {
// triggers after 0.3 seconds, and of course, this cannot error
}
// ^^ There is no `catch`; you *cannot* write one. Errors are not possible.
The key difference is the lack of the need for a catch
, and in addition, we
use Swift to inform you of this, Promise
’s then
does not declare
@discardableResult
, so you get a warning to do something with the result of
your then
, that is, you must catch
or return the promise thus punting the
warning to a higher level, where you there will need to catch
:
fetch().then {
//…
}
// ^^ Swift will warn “unused result” which is your hint that you forgot error handling
// NOTE `fetch` returns Promise, you’d get *no warning* if it returned Guarantee
fetch().then {
//…
}.catch {
//…
}
// ^^ no warning
return fetch().then {
//…
}
// ^^ warning is punted to the caller of this function,
// ie. caller takes responsibilty for error handling
Guarantee
s make using PromiseKit as error-safe as Swift, thus they match the
Swift error system’s intent and since Promises are like do
, try
, catch
blocks but for asynchronicity: this works well.
Another marvelous outcome of this is our when(resolved:)
variant. This when
always resolves because it resolves with an array of Result
, thus it cannot
error. With PMK4 you had to just know that you didn’t need to catch
with
PMK 6 Swift itself knows you cannot catch
because it returns a Guarantee
.
when(resolved: promises).done { results in
//…
}
// ^^ There is no `catch`; you *cannot* write one. Errors are not possible.
Note we also provide
when(fulfilled:)
which rejects if any of the promises it is waiting onreject
. Generally this is the version you should use but when you need all promises to finish even if they error you can use the abovewhen(resolved:)
variant and the language itself will make it clear that there can be no error handling!
Note sometimes you just don’t care about the
catch
(you monster you!), so we providecauterize()
use it to terminate a chain, log any error and remove the warning from yourPromise
chain.
Thenable
Guarantees and Promises both conform to the same protocol Thenable
and thus
can be combined. If you return a Promise from a Guarantee
’s then
(or if you
throw) the chain becomes a Promise chain. If you use recover
on a Promise
chain it becomes a Guarantee
.
In use we find the introduction of Guarantee
fabulous and very “Swifty”.
Notably returning a
Guarantee
from aPromise
’sthen
does not cause thatthen
to return aGuarantee
. Any part of a previous chain could have error’d even if that specificthen
does not, so we cannot become aGuarantee
unless yourecover
(and in order to remain aGuarantee
your recover handler cannotthrow
!) first.
For the first year running we didn’t need a new major version for Swift 4. Swift 3 and 4 had very few breaking language or stdlib changes. Thus PromiseKit 4 & 6 both support Swift 3 & 4.
Using PromiseKit has been delightful… and awful. Swift is great… and terrible, specifically its error diagnostics are so bad in certain conditions that I honestly have started to wonder if recommending the language is still a good idea. Like, Javascript is terrible, but at least when it goes wrong you can figure out what you have to do to fix your code. Swift lies.
If there is an error inside a closure, Swift, with no improvements since v1 :(, has told you the wrong error.
PromiseKit is all closures. So you can see our problem.
I believe in tools that are as easy to get up to speed with as possible, the idea that you should have to spend time learning how to use something so that you can get your work done quicker is nonsense. You should reject that. We do.
With PromiseKit our then
did multiple things, and we relied on Swift to infer
the correct then
from context. However with multiple line then
s it would
fail to do this, and instead of telling you that the situation was ambiguous it
would invent some other error. Often the dreaded
cannot convert T to AnyPromise
. We have a troubleshooting guide to combat this
but I believe in tools that just work, and when you spend 4 years waiting for
Swift to fix the issue and Swift doesn’t fix the issue, what do you do? We chose
to find a solution at the higher level.
So we split then
into then
, done
and map
.
then
is fed the previous promise value and requires you return a promise.done
is fed the previous promise value and returns a Void
promise (which is 80% of chain usage)map
is fed the previous promise value and requires you return a non-promise, ie. a value.At first I was nervous about this. But with some use on real projects I quickly
realized that done
alone was making PromiseKit use much more pleasant. Because
Swift has no inference to do about the return for done
you can write many line
closures without any pain. then
and map
still require you to specify the
return types for closures if they are multiple line, but often they are single
line because you are chaining promises encapsulated in other functions.
The result is a happier compiler, a happier you and also, pleasantly (and somewhat surprisingly), clearer intent for your chains.
compactMap
Having added map
desire for other functional primitives began. So we tried
compactMap
. Probably the most useful new introduction. So many Swift methods
return an optional (if let
is a great language feature after all), in a
promise chain you need to functionally transform values, and compactMap
lets
you get error transmission when nil
is returned, for example:
firstly {
URLSession.shared.dataTask(.promise, with: url)
}.compactMap {
String(data: $0.data, encoding: .utf8) // returns `String?`
}.done {
//…
}.catch {
// though probably you should return without the `catch`
}
See the composability section later, but compactMap
alone allowed us to reduce
the magic of PromiseKit and make it more immediately understandable. Previously
our URLSession
promises were more black-boxed returning a magic object that
made their use sometimes tedious for the sake of adding methods like asImage()
to facilitate their use in chains. Now if you need an image from a dataTask you
can compose it:
firstly {
URLSession.shared.dataTask(.promise, with: url)
}.compactMap { data, urlResponse in
UIImage(data: data)
}.done {
//…
}
Note we released
compactMap
asflatMap
, see discussion here.
get
get
is done
but it returns the same value that your handler is fed:
firstly {
.value(1)
}.get { foo in
print(foo, " is 1")
}.done { foo in
print(foo, " is 1")
}
This is also a common pattern, hence once we had started supplementing your toolkit we added this also.
tap
An insertion for chain debug:
firstly {
.value(1)
}.tap { result in
print(foo, " is Result.fulfilled(1)")
}.done { foo in
print(foo, " is 1")
}
tap
feeds you the current Result<T>
for the chain, so is called if the
chain is succeeding or if it is failing.
lastValue
, firstValue
, filterValues
, compactMap
, etc.We have added many of the other functional primitives that Sequence
have
including logical extensions like thenMap
.
firstly {
return when(fulfilled: promisesForDataFetches)
}.map {
String(data: $0) // runs for each data from the `when`
}.thenMap {
fetch(stringUrl: $0) // fetch returns promise, runs once for each string
}.done { arrayOfFetchedThings in
// finally you have an array of things!
}
Promise.init
We altered the main initializer:
Promise { fulfill, reject in
//…
}
You now have:
Promise { seal in
// seal.fulfill(foo)
// seal.reject(error)
// seal.resolve(foo, error)
}
We considered adding a new initializer that provided the seal
object, but this
led to the usual Swift ambiguity issues despite the different parameter counts.
So sadly, in order to progress we have had to change a fundamental.
However there are good reasons for it. The seal
has many overrides of
resolve
so you can typically just pass seal.resolve
to a completion handler
and Swift will automatically figure out the types:
func myFunction(withCompletion: (String?, Error?) -> Void) {
//…
}
Promise {
myFunction(withCompletion: $0.resolve)
}.then { foo in
// foo is `String`!
}.catch {
// errors from myFunction are handled!
}
We also provide specific variants of the Sealant
object for Void
and Bool
completions.
We have attempted to make all the extensions more useful and more composable.
For example CLLocationManager.promise()
used to return LocationPromise
which
inherited Promise<CLLocation>
and had a function on it that returned
Promise<[CLLocation]>
so that you could get all the locations that may have
been returned during the CLLocationManager
’s updateLocation period.
Now we return Promise<[CLLocation]>
so there is no new class to look at or
understand. Instead we provide a .last
(or .first
) method. Which you should
use:
firstly {
CLLocationManager.requestLocation()
}.last.done { location in
//…
}
If the array is empty PromiseKit fails the chain (say hi in your catch
).
Otherwise you get one result.
--
For some of our extensions we made using Apple’s frameworks less good. Ambiguity resulted. For example:
let promise = URLSession.shared.dataTask(with: url)
Swift cannot infer the type of promise
because PromiseKit and Foundation both
provide versions of this method (Foundation’s returns URLDataTask
).
Usually you didn’t have problems because when returning into chains Swift could figure out which you wanted. Nonetheless we have fixed this, now you do:
let promise = URLSession.shared.dataTask(.promise, with: url)
Swifty, readable, clear and the compiler is happy.
In general we have tried to improve naming and align as closely as possible to the Apple functions we mirror. For example:
CLLocationManager.promise() -> CLLocationManager.requestLocation()
UIView.promiseAnimation(withDuration: 0.3) -> UIView.animate(.promise, duration: 0.3)
DispatchQueue.global().promise -> DispatchQueue.global().async(.promise)
We released 5 to Carthage, but weren’t entirely confident in it. In the end this proved right, we didn’t remove all ambiguity. This constructor was evil:
Promise(value: T)
Swift is greedy and would try to use this constructor too readily. For example:
let p = Promise { fulfill, reject in
fulfill(1)
}
What should p
be? Hopefully Promise<Int>
but actually, sometimes,
Promise<(_->Void, (Error)->Void)>
. Yeah. What the…? Well it’s because
of trailing closure syntax, and Swift greedily trying to use this initializer
even though there is the much better Promise(resolver:)
intializer that
fits this syntax exactly.
When this happened you’d almost always spot it, however we had situations where it got to production due to Swift type inference hiding the mis-identification of the type from us.
So we removed it. So now if you need a resolved promise use:
return .value(1)
Which is a static method on Promise<T>
.
By default all PromiseKit handlers dispatch to .main
, this is safest and thus
the default.
However we heartily recommend you change the default queue for then
, map
and
the other “transforming” functions to a background queue.
PromiseKit has always allowed you to change the default queue, but 6 goes a little further and distinguishes between the two main kinds of handler: those that transform values and usually are stateless, and those that finalize chains and usually modify application state. It’s the latter that you almost always want on the main queue since it acts as an easy form of synchronization:
PromiseKit.conf.Q.map = .global()
PromiseKit.conf.Q.return = .main // FYI this is the default
Especially now Xcode 9 gives a runtime warning for using function that must be on the main queue in the background. This is a low-risk, high-gain tweak for your apps.
Note sorry about these names. I missed the
TODO
to fix them before release…
You can now define that the default catch policy for all recover
and catch
by allErrors
rather than allErrorsExceptCanncellation
by changing
conf.catchPolicy
.
To learn more about PromiseKit’s cancellation system, see the dedicated part of our documentation at GitHub.
Promise { fulfill, reject in
asyncThing { value, error in
if let value = value {
fulfill(value)
} else if let error = error {
reject(error)
} else {
reject(PMKError.invalidCallingConvention)
}
}
}
Becomes:
Promise { seal in
asyncThing { value, error in
seal.resolve(value, error)
}
}
You can even go this far for completion-handler only systems:
Promise(resolver: asyncThing)
Promise(value:)
We removed this initializer (rationale is above), so:
return Promise(value: foo)
Becomes:
return .value(foo)
then
You may need to convert your then
s into done
or map
. Explore compactMap
also since it can really help you to write quick chains that behave exactly as
you want.
Guarantee
If your chain cannot fail try to use Guarantees. One way to force a guarantee
is to use recover
to recover all errors:
foo().recover{_ in}.done { foo in
//…
}
Use this carefully! Ideally you’d just convert foo()
to return a Guarantee
,
aim for the lowest level where there are no errors and switch that over.
wrap
wrap
is no longer provided, use Promise(resolver:)
:
return PromiseKit.wrap(start)
Becomes:
return Promise { start(completionHandler: $0.resolve) }
It was always desired to have wrap
be a Promise
initializer for clarity
reasons, but it wasn’t possible until Swift 3.1 allowed us to specialize
extensions. So now we can do it, we do.
always
always
is now ensure
.
Promisable
Previously we provided a Promisable
protocol as part of our UIKit extensions
to facilitate using promises with UIViewController
presentation and
dismissals.
I’m sorry if you depended on this because we have removed it. We feel it was not a good general solution and we feel it was a bad pattern that violated the encapsulation of your view-controller heirarchy.
Instead we suggest adding some minimal code to your view controllers that you want to be governed by a promise. To be clear this still violates encapsulation because the viewController dismisses itself, but that’s your decision to make (I do it in my apps!), it’s just that the library itself should not promote this pattern.
class ViewController: UIViewController {
private let (promise, seal) = Promise<…>.pending() // use Guarantee if your flow can’t fail
func show(in: UIViewController) -> Promise<…> {
in.show(self, sender: in)
return promise
}
// you will need to call this instead of `dismiss`
// if there's a sure-fire way to know when a vc is dismissed, Apple don't document it.
func done() {
dismiss(animated: true)
seal.fulfill(…)
}
}
PMKAlertController
Removed. This was not good enough a model for the library, by all means grab it from PMK4 and use it, but we should not encourage this generally. Instead you can easily add promises yourself:
Promise { seal in
let ac = UIAlertController(…)
ac.addAction(.init(…, completionHandler: seal.fulfill))
ac.addAction(.init(…, completionHandler: seal.reject))
present(ac, animated: true)
}.done { _ in
//…
}.catch { _ in
//…
}
Doing it yourself gives you control over what fulfilled and rejected mean in your own contexts. Or just use a Guarantee if there is no error condition.
If you must unleash zalgo, we now accept nil
as the queue for any handler,
which aligns us more closely with what nil
(usually) means with Apple’s
APIs for queue type parameters.
then(execute:)
Because it reads better we dropped the execute:
parameter name for then
, so:
fetch().then(execute: layout)
Becomes:
fetch().then(layout)
Of course this only applies when you pass functions directly to then
.
.catch{ /*…*/ }.finally
In PMK 4 catch
returned the promise it was attached to. This led to unexpected
behavior for many people and was a mistake. Sorry.
However, often it is useful to have what is an ensure
and to have it occur
after your catch
handler. Thus we have finally
(named because it really is
finally).
spinner(visible: true)
firstly {
foo()
}.done {
//…
}.catch {
//…
}.finally {
self.spinner(visible: false)
}
Note, indeed you cannot do anything else after a
catch
.catch
is a chain- terminator, if we allowed you to generally chain off of it it would easily lead to situations of ambiguity for you, what should happen if youcatch
after acatch
? What is the value of the chain after thecatch
? These are questions that could have multiple answers.
If you want the previous behavior, either use recover
or do what the previous
catch
did, ie. return the promise:
let p = somePromise()
//…
p.catch { /**/ }
return p
Relunctantly this is gone. A fabulous feature, but maintaining it was quite a
burden. Partly the reason we justify this is now it is quite hard to not
handle errors due to Guarantee
and Swift warning when you don’t add catch
to your chains. Still there are scenarios that you can concoct where by you
could have chains without error-handling. Ideally we would bring this back, but
it is immensely intrusive to our codebase. PR welcome.
The bridging between Error and NSError and supporting cancelation on top of this was tricky. In the end rather than try to anticipate every possibility we removed this feature.
NOTE The Swift interface for AnyPromise still supports cancellation.
To work around this you can make your own @objc
Swift extension for NSError
that can inform you about cancellations you are interested in.
NSError.cancelledError
thus has been removed, you can use PMKError.cancelled
instead now.
We do not have many deprecations, so your code may stop compiling if you upgrade. The reason for the lack of deprecation notices is again: ambiguity. Swift tends to pick even deprecated versions of ambiguous functions, and this led to pain when writing new code.
We suggest looking at the sources for the extensions we provide should you need to. The code is neatly organized and easy to read.
Swift still sucks for error diagnostics, and it is a massive barrier to entry for newcomers to programming (I teach newcomers to programming at my coding school).
For example PromiseKit still sucks because of Swift errors, here’s an example:
try service.fetchAll().then { result in
self.projects = result.projects
}
This errors. The issue is: then
requires you to return a Promise
and nothing
is returning. So what error does Swift give us?
error: Type of expression is ambiguous without more context
While error messages are basically useless as much as half the time Swift will lose mind-share and respect. I see it with new devs, they shake their heads and then tell me they prefer Javascript, they then start reading up about React Native.
I even see experienced developers not know how to proceed here, and this is for the additional reason that getting Xcode to show you the function definition for Swift code is a toss-up, half the time it doesn’t work, so experienced devs have stopped trying to make it work. When you can’t trust your tools you can’t get things done.
We really don’t expect any further large changes. We believe we have correctly applied promises to Swift and that Swift itself won’t change that much in the future.
Welcome! Open a ticket.