4.4 KiB
Promise and Future
When working with asynchronous APIs, one of the problems you'll face is not knowing when a variable is set.
When querying a database synchronously, the thread is blocked until a result has been received. At which point the result will be returned to you and the thread continues from where you left off querying the database.
let user = try database.fetchUser(named: "Admin")
print(user.username)
In the asynchronous world, you won't receive a result immediately. Instead, you'll receive a result in a callback.
// Callback `found` will receive the user. If an error occurred, the `onError` callback will be called instead.
try database.fetchUser(named: "Admin", found: { user in
print(user.username)
}, onError: { error in
print(error)
})
You can imagine code becoming complex. Difficult to read and comprehend.
Promises and futures are two types that this library introduces to solve this.
Creating a promise
Promises are important if you're implementing a function that returns a result in the future, such as the database shown above.
Promises need to be created without a result. They can then be completed with the expectation or an error at any point.
You can extract a future from the promise that you can hand to the API consumer.
// the example `fetchUser` implementation
func fetchUser(named name: String) -> Future<User> {
// Creates a promise that can be fulfilled in the future
let promise = Promise<User>()
// Run a query asynchronously, looking for the user
asyncDatabaseQuery(where: "username" == name, onComplete: { databaseResult in
do {
// Initialize the user using the datbase result
// This can throw an error if the result is empty or invalid
let user = try User(decodingFrom: databaseResult)
// If initialization is successful, complete the promise.
//
// Completing the promise will notify the promise's associated future with this user
promise.complete(user)
} catch {
// If initialization is successful, fail the promise.
//
// Failing the promise will notify the promise's associated future with an error
promise.fail(error)
}
})
// After spawning the asynchronous operation, return the promise's associated future
//
// The future can then be used by the API consumer
return promise.future
}
On future completion
When a promise completes, you can chain the result/error into a closure:
// The future provided by the above function will be used
let future: Future<User> = fetchUser(named: "Admin")
// `.then`'s closure will be executed on success
future.then { user in
print(user.username)
// `.catch` will catch any error on failure
}.catch { error in
print(error)
}
Catching specific errors
Sometimes you only care for specific errors, for example, for logging.
// The future provided by the above function will be used
let future: Future<User> = fetchUser(named: "Admin")
// `.then`'s closure will be executed on success
future.then { user in
print(user.username)
// This `.catch` will only catch `DatabaseError`s
}.catch(DatabaseError.self) { databaseError in
print(databaseError)
// `.catch` will catch any error on failure, including `DatabaseError` types
}.catch { error in
print(error)
}
Mapping results
Futures can be mapped to different results asynchronously.
// The future provided by the above function will be used
let future: Future<User> = fetchUser(named: "Admin")
// Maps the user to it's username
let futureUsername: Future<String> = future.map { user in
return user.username
}
// Mapped futures can be mapped and chained, too
futureUsername.then { username in
print(username)
}
For synchronous APIs
Sometimes, an API needs to be used synchronously in a synchronous envinronment.
Rather than using a synchronous API with all edge cases involved, we recommend using the try future.sync() function.
// The future provided by the above function will be used
let future: Future<User> = fetchUser(named: "Admin")
// This will either receive the user if the promise was completed or throw an error if the promise was failed.
let user: User = try future.sync()
This will wait for a result indefinitely, blocking the thread.
If you expect a result with a specified duration, say, 30 seconds:
// This will also throw an error if the deadline wasn't met
let user = try future.sync(deadline: .seconds(30))