:clapper: Tame async code with battle-tested promises

Then
Reason - Example - Documentation - Installation
fetchUserId().then { id in
print("UserID : \(id)")
}.onError { e in
print("An error occured : \(e)")
}.finally {
print("Everything is Done :)")
}
let userId = try! awaitPromise(fetchUserId())
Because async code is hard to write, hard to read, hard to reason about. A pain to maintain
Try it
then is part of freshOS iOS toolset. Try it in an example App! Download Starter ProjectHow
By using a then keyword that enables you to write aSync code that reads like an English sentence Async code is now concise, flexible and maintainable โค๏ธWhat
- [x] Based on the popular
Promise/Futureconcept - [x]
Async/Await - [x]
progressracerecovervalidateretrybridgeErrorchainnoMatterWhat... - [x] Strongly Typed
- [x] Pure Swift & Lightweight
Example
Before
fetchUserId({ id in
fetchUserNameFromId(id, success: { name in
fetchUserFollowStatusFromName(name, success: { isFollowed in
// The three calls in a row succeeded YAY!
reloadList()
}, failure: { error in
// Fetching user ID failed
reloadList()
})
}, failure: { error in
// Fetching user name failed
reloadList()
})
}) { error in
// Fetching user follow status failed
reloadList()
}
๐๐๐#callbackHell
After
fetchUserId()
.then(fetchUserNameFromId)
.then(fetchUserFollowStatusFromName)
.then(updateFollowStatus)
.onError(showErrorPopup)
.finally(reloadList)
๐๐๐
Going further ๐ค
fetchUserId().then { id in
print("UserID : \(id)")
}.onError { e in
print("An error occured : \(e)")
}.finally {
print("Everything is Done :)")
}
If we want this to be maintainable, it should read like an English sentence We can do this by extracting our blocks into separate functions:
fetchUserId()
.then(printUserID)
.onError(showErrorPopup)
.finally(reloadList)
This is now concise, flexible, maintainable, and it reads like an English sentence <3 Mental sanity saved // #goodbyeCallbackHell
Documentation
- Writing your own Promise
- Progress
- Registering a block for later
- Returning a rejecting promise
- Common Helpers
Writing your own Promise ๐ช
Wondering what fetchUserId() is? It is a simple function that returns a strongly typed promise :func fetchUserId() -> Promise<Int> {
return Promise { resolve, reject in
print("fetching user Id ...")
wait { resolve(1234) }
}
}
Here you would typically replace the dummy wait function by your network request <3
Progress
As for then and onError, you can also call a progress block for things like uploading an avatar for example.
uploadAvatar().progress { p in
// Here update progressView for example
}
.then(doSomething)
.onError(showErrorPopup)
.finally(doSomething)
Registering a block for later
Our implementation slightly differs from the original javascript Promises. Indeed, they do not start right away, on purpose. Callingthen, onError, or finally will start them automatically.
Calling then starts a promise if it is not already started. In some cases, we only want to register some code for later. For instance, in the case of JSON to Swift model parsing, we often want to attach parsing blocks to JSON promises, but without starting them.
In order to do that we need to use registerThen instead. It's the exact same thing as then without starting the promise right away.
let fetchUsers:Promise<[User]> = fetchUsersJSON().registerThen(parseUsersJSON)
// Here promise is not launched yet \o/
// later... fetchUsers.then { users in // YAY }
Note that onError and finally also have their non-starting counterparts : registerOnError and registerFinally.
Returning a rejecting promise
Oftetimes we need to return a rejecting promise as such :
return Promise { _, reject in
reject(anError)
}
This can be written with the following shortcut :
return Promise.reject(error:anError)
Common Helpers
Race
With race, you can send multiple tasks and get the result of the first one coming back :
race(task1, task2, task3).then { work in // The first result ! }
Recover
With .recover, you can provide a fallback value for a failed Promise. You can : - Recover with a value - Recover with a value for a specific Error type - Return a value from a block, enabling you to test the type of error and return distinct values. - Recover with another Promise with the same Type
.recover(with: 12)
.recover(MyError.defaultError, with: 12)
.recover { e in
if e == x { return 32 }
if e == y { return 143 }
throw MyError.defaultError
}
.recover { e -> Promise<Int> in
// Deal with the error then
return Promise<Int>.resolve(56)
// Or
return Promise<Int>.reject(e)
}
}
.recover(with: Promise<Int>.resolve(56))
Note that in the block version you can also throw your own error \o/
Validate
With .validate, you can break the promise chain with an assertion block.
You can: - Insert assertion in Promise chain - Insert assertion and return you own Error
For instance checking if a user is allowed to drink alcohol :
fetchUserAge() .validate { $0 > 18 } .then { age in // Offer a drink }retry.validate(withError: MyError.defaultError, { $0 > 18 })
</code></pre> A failed validation will retrun aPromiseError.validationFailedby default.Retry
With
, you can restart a failed Promise X number of times. <pre><code class="lang-swift">doSomething() .retry(10) .then { v in // YAY! }.onError { e in // Failed 10 times in a row }</code></pre>.bridgeErrorBridgeError
With
, you can intercept a low-level Error and return your own high level error. The classic use-case is when you receive an api error and you bridge it to your own domain error..whenAllYou can: - Catch all errors and use your own Error type - Catch only a specific error
<pre><code class="lang-swift">.bridgeError(to: MyError.defaultError) .bridgeError(SomeError, to: MyError.defaultError)</code></pre>
WhenAll
With
, you can combine multiple calls and get all the results when all the promises are fulfilled :chain<pre><code class="lang-swift">whenAll(fetchUsersA(),fetchUsersB(), fetchUsersC()).then { allUsers in // All the promises came back }</code></pre>
Chain
With
, you can add behaviours without changing the chain of Promises.noMatterWhatA common use-case is for adding Analytics tracking like so:
<pre><code class="lang-swift">extension Photo { public func post() -> Async<Photo> { return api.post(self).chain { _ in Tracker.trackEvent(.postPicture) } } }</code></pre>
NoMatterWhat
With
you can add code to be executed in the middle of a promise chain, no matter what happens.unwrap<pre><code class="lang-swift">func fetchNext() -> Promise<[T]> { isLoading = true call.params["page"] = page + 1 return call.fetch() .registerThen(parseResponse) .resolveOnMainThread() .noMatterWhat { self.isLoading = false } }</code></pre>
Unwrap
With
you can transform an optional into a promise :unwrappingFailed<pre><code class="lang-swift">func fetch(userId: String?) -> Promise<Void> { return unwrap(userId).then { network.get("/user/\($0)") } }</code></pre> Unwrap will fail the promise chain with
error in case of a nil value :)AsyncTaskAsyncTask
andAsynctypealisases are provided for those of us who think that Async can be clearer thanPromise. Feel free to replacePromisebyAsyncTaskandPromisebyAsyncwherever needed. This is purely for the eyes :)awaitPromiseAsync/Await
waits for a promise to complete synchronously and yields the result :async<pre><code class="lang-swift">let photos = try! awaitPromise(getPhotos())</code></pre>
takes a block and wraps it in a background Promise.!<pre><code class="lang-swift">async { let photos = try awaitPromise(getPhotos()) }</code></pre> Notice how we don't need the
anymore becauseasyncwill catch the errors.asyncTogether,
/awaitPromiseenable us to write asynchronous code in a synchronous manner :..<pre><code class="lang-swift">async { let userId = try awaitPromise(fetchUserId()) let userName = try awaitPromise(fetchUserNameFromId(userId)) let isFollowed = try awaitPromise(fetchUserFollowStatusFromName(userName)) return isFollowed }.then { isFollowed in print(isFollowed) }.onError { e in // handle errors }</code></pre>
Await operators
Await comes withshorthand operator. The..?will fallback to a nil value instead of throwing. <pre><code class="lang-swift">let userId = try awaitPromise(fetchUserId())</code></pre> Can be written like this: <pre><code class="lang-swift">let userId = try ..fetchUserId()</code></pre>ThenInstallation
The Swift Package Manager (SPM) is now the official way to install
. The other package managers are now deprecated as of5.1.3and won't be supported in future versions.XcodeSwift Package Manager
>File>Swift Packages>Add Package Dependency...>Pastehttps://github.com/freshOS/Then`Cocoapods - Deprecated
target 'MyApp' pod 'thenPromise' use_frameworks!Carthage - Deprecated
github "freshOS/then"Contributors
S4cha, Max Konovalov, YannickDot, Damien, piterlouis
Swift Version
- Swift 2 -> version 1.4.2
- Swift 3 -> version 2.2.5
- Swift 4 -> version 3.1.0
- Swift 4.1 -> version 4.1.1
- Swift 4.2 -> version 4.2.0
- Swift 4.2.1 -> version 4.2.0
- Swift 5.0 -> version 5.0.0
- Swift 5.1 -> version 5.1.0
- Swift 5.1.3 -> version 5.1.2
Backers
Like the project? Offer coffee or support us with a monthly donation and help us continue our activities :)Sponsors
Become a sponsor and get your logo on our README on Github with a link to your site :)