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
Guarantees 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 yourPromisechain.
ThenableGuarantees 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
Guaranteefrom aPromise’sthendoes not cause thatthento return aGuarantee. Any part of a previous chain could have error’d even if that specificthendoes not, so we cannot become aGuaranteeunless yourecover(and in order to remain aGuaranteeyour 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 thens 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.
compactMapHaving 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
compactMapasflatMap, see discussion here.
getget 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.
tapAn 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.initWe 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
TODOto 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)
thenYou may need to convert your thens into done or map. Explore compactMap
also since it can really help you to write quick chains that behave exactly as
you want.
GuaranteeIf 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.
wrapwrap 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.
alwaysalways is now ensure.
PromisablePreviously 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(…)
}
}
PMKAlertControllerRemoved. 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{ /*…*/ }.finallyIn 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.catchis 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 youcatchafter 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.