4.
Async
Written by Tim Condon
In this chapter, you’ll learn about asynchronous and non-blocking architectures. You’ll discover Vapor’s approach to these architectures and how to use it. Finally, the chapter provides a small overview of SwiftNIO, a core technology used by Vapor.
Async
One of Vapor’s most important features is Async
. It can also be one of the most confusing. Why is it important?
Consider a scenario where your server has only a single thread and four client requests, in order:
- A request for a stock quote. This results in a call to an API on another server.
- A request for a static CSS style sheet. The CSS is available immediately without a lookup.
- A request for a user’s profile. The profile must be fetched from a database.
- A request for some static HTML. The HTML is available immediately without a lookup.
In a synchronous server, the server’s sole thread blocks until the stock quote is returned. It then returns the stock quote and the CSS style sheet. It blocks again while the database fetch completes. Only then, after the user’s profile is sent, will the server return the static HTML to the client.
On the other hand, in an asynchronous server, the thread initiates the call to fetch the stock quote and puts the request aside until it completes. It then returns the CSS style sheet, starts the database fetch and returns the static HTML. As the requests that were put aside complete, the thread resumes work on them and returns their results to the client.
“But, wait!”, you say, “Servers have more than one thread.” And you’re correct. However, there are limits to how many threads a server can have. Creating threads uses resources. Switching context between threads is expensive, and ensuring all your data accesses are thread-safe is time-consuming and error-prone. As a result, trying to solve the problem solely by adding threads is a poor, inefficient solution.
Futures and promises
In order to “put aside” a request while it waits for a response, you must wrap it in a promise to resume work on it when you receive the response.
In practice, this means you must change the return type of methods that can be put aside. In a synchronous environment, you might have a method:
func getAllUsers() -> [User] {
// do some database queries
}
In an asynchronous environment, this won’t work because your database call may not have completed by the time getAllUsers()
must return. You know you’ll be able to return [User]
in the future but can’t do so now. In Vapor, you return the result wrapped in an EventLoopFuture. This is a future specific to SwiftNIO’s EventLoop
. You’d write your method as shown below:
func getAllUsers() -> EventLoopFuture<[User]> {
// do some database queries
}
Returning EventLoopFuture<[User]>
allows you to return something to the method’s caller, even though there may be nothing to return at that point. But the caller knows that the method returns [User]
at some point in the future. You’ll learn more about SwiftNIO at the end of the chapter.
Working with futures
Working with EventLoopFuture
s can be confusing at first but, since Vapor uses them extensively, they’ll quickly become second nature. In most cases, when you receive an EventLoopFuture
from a method, you want to do something with the actual result inside the EventLoopFuture
. Since the result of the method hasn’t actually returned yet, you provide a callback to execute when the EventLoopFuture
completes.
In the example above, when your program reaches getAllUsers()
, it makes the database request on the EventLoop
. An EventLoop
processes work and in simplistic terms can be thought of as a thread. getAllUsers()
doesn’t return the actual data immediately and returns an EventLoopFuture
instead. This means the EventLoop
pauses execution of that code and works on any other code queued up on that EventLoop
. For example, this could be another part of your code where a different EventLoopFuture
result has returned. Once the database call returns, the EventLoop
then executes the callback.
If the callback calls another method that returns an EventLoopFuture
, you provide another callback inside the original callback to execute when the second EventLoopFuture
completes. This is why you’ll end up chaining or nesting lots of different callbacks. This is the hard part about working with futures. Asynchronous methods require a complete shift in how to think about your code.
Resolving futures
Vapor provides a number of convenience methods for working with futures to avoid the necessity of dealing with them directly. However, there are numerous scenarios where you must wait for the result of a future. To demonstrate, imagine you have a route that returns the HTTP status code 204 No Content. This route fetches a list of users from a database using a method like the one described above and modifies the first user in the list before returning.
In order to use the result of that call to the database, you must provide a closure to execute when the EventLoopFuture
has resolved. There are two main methods you’ll use to do this:
-
flatMap(_:): Executes on a future and returns another future. The callback receives the resolved future and returns another
EventLoopFuture
. -
map(_:): Executes on a future and returns another future. The callback receives the resolved future and returns a type other than
EventLoopFuture
, whichmap(_:)
then wraps in anEventLoopFuture
.
Both choices take a future and produce a different EventLoopFuture
, usually of a different type. To reiterate, the difference is that if the callback that processes the EventLoopFuture
result returns an EventLoopFuture
, use flatMap(_:)
. If the callback returns a type other than EventLoopFuture
, use map(_:)
.
For example:
// 1
return database.getAllUsers().flatMap { users in
// 2
let user = users[0]
user.name = "Bob"
// 3
return user.save(on: req.db).map {
//4
return .noContent
}
}
Here’s what this does:
- Fetch all users from the database. As you saw above,
getAllUsers()
returnsEventLoopFuture<[User]>
. Since the result of completing thisEventLoopFuture
is yet anotherEventLoopFuture
(see step 3), useflatMap(_:)
to resolve the result. The closure forflatMap(_:)
receives the completed futureusers
— an array of all the users from the database, type[User]
— as its parameter. This.flatMap(_:)
returnsEventLoopFuture<HTTPStatus>
. - Update the first user’s name.
- Save the updated user to the database. This returns
EventLoopFuture<Void>
but the HTTPStatus value you need to return isn’t yet anEventLoopFuture
so usemap(_:)
. - Return the appropriate
HTTPStatus
value.
As you can see, for the top-level promise you use flatMap(_:)
since the closure you provide returns an EventLoopFuture
. The inner promise, which returns a non-future HTTPStatus
, uses map(_:)
.
Transform
Sometimes you don’t care about the result of a future, only that it completed successfully. In the above example, you don’t use the resolved result of save(on:)
and are returning a different type. For this scenario, you can simplify step 3 by using transform(to:)
:
return database.getAllUsers().flatMap { users in
let user = users[0]
user.name = "Bob"
return user
.save(on: req.db)
.transform(to: HTTPStatus.noContent)
}
This helps reduce the amount of nesting and can make your code easier to read and maintain. You’ll see this used throughout the book.
Flatten
There are times when you must wait for a number of futures to complete. One example occurs when you’re saving multiple models in a database. In this case, you use flatten(on:)
. For instance:
static func save(_ users: [User], request: Request)
-> EventLoopFuture<HTTPStatus> {
// 1
var userSaveResults: [EventLoopFuture<Void>] = []
// 2
for user in users {
userSaveResults.append(user.save(on: request.db))
}
// 3
return userSaveResults
.flatten(on: request.eventLoop)
.map {
// 4
for user in users {
print("Saved \(user.username)")
}
// 5
return .created
}
}
In this code, you:
- Define an array of
EventLoopFuture<User>
, the return type ofsave(on:)
in step 2. - Loop through each user in
users
and append the return value ofuser.save(on:)
to the array. - Use
flatten(on:)
to wait for all the futures to complete. This takes anEventLoop
, essentially the thread that actually performs the work. This is normally retrieved from aRequest
in Vapor, but you’ll learn about this later. The closure forflatten(on:)
, if needed, takes the returned collection as a parameter. In this case it’sVoid
. - Loop through each of the users now you’ve saved them and print out their usernames.
- Return a 201 Created status.
flatten(on:)
waits for all the futures to return as they’re executed asynchronously by the same EventLoop
.
Multiple futures
Occasionally, you need to wait for a number of futures of different types that don’t rely on one another. For example, you might encounter this situation when retrieving users from the database and making a request to an external API. SwiftNIO provides a number of methods to allow waiting for different futures together. This helps avoid deeply nested code or confusing chains.
If you have two futures — get all the users from the database and get some information from an external API — you can use and(_:)
like this:
// 1
getAllUsers()
// 2
.and(req.client.get("http://localhost:8080/getUserData"))
// 3
.flatMap { users, response in
// 4
users[0].addData(response).transform(to: .noContent)
}
Here’s what this does:
- Call
getAllUsers()
to get the result of the first future. - Use
and(_:)
to chain the second future to the first future. - Use
flatMap(_:)
to wait for the futures to return. The closure takes the resolved results of the futures as parameters. - Call
addData(_:)
, which returns some future result and transform the return to.noContent
.
If the closure returns a non-future result, you can use map(_:)
on the chained futures instead:
// 1
getAllUsers()
// 2
.and(req.client.get("http://localhost:8080/getUserData"))
// 3
.map { users, response in
// 4
users[0].syncAddData(response)
// 5
return .content
}
Here’s what this does:
- Call
getAllUsers()
to get the result of the first future. - Use
and(_:)
to chain the second future to the first future. - Use
map(_:)
to wait for the futures to return. The closure takes the resolved results of the futures as parameters. - Call the synchronous
syncAddData(_:)
- Return
.noContent
.
Note: You can chain together as many futures as required with and(_:)
but the flatMap
or map
closure returns the resolved futures in tuples. For instance, for three futures:
getAllUsers()
.and(getAllAcronyms())
.and(getAllCategories()).flatMap { result in
// Use the different futures
}
result
is of type (([User], [Acronyms]), [Categories])
. And the more futures you chain with and(_:)
, the more nested tuples you get. This can get a bit confusing! :]
Creating futures
Sometimes you need to create your own futures. If an if
statement returns a non-future and the else
block returns an EventLoopFuture
, the compiler will complain that these must be the same type. To fix this, you must convert the non-future into an EventLoopFuture
using request.eventLoop.future(_:)
. For example:
// 1
func createTrackingSession(for request: Request)
-> EventLoopFuture<TrackingSession> {
return request.makeNewSession()
}
// 2
func getTrackingSession(for request: Request)
-> EventLoopFuture<TrackingSession> {
// 3
let session: TrackingSession? =
TrackingSession(id: request.getKey())
// 4
guard let createdSession = session else {
return createTrackingSession(for: request)
}
// 5
return request.eventLoop.future(createdSession)
}
Here’s what this does:
- Define a method that creates a
TrackingSession
from the request. This returnsEventLoopFuture<TrackingSession>
. - Define a method that gets a tracking session from the request.
- Attempt to create a tracking session using the request’s
key
. This returnsnil
if the tracking session could not be created. - Ensure the session was created successfully, otherwise create a new tracking session.
- Create an
EventLoopFuture<TrackingSession>
fromcreatedSession
usingrequest.eventLoop.future(_:)
. This returns the future on the request’sEventLoop
.
Since createTrackingSession(for:)
returns EventLoopFuture<TrackingSession>
you have to use request.eventLoop.future(_:)
to turn the createdSession
into an EventLoopFuture<TrackingSession>
to make the compiler happy.
Dealing with errors
Vapor makes heavy use of Swift’s error handling throughout the framework. Many methods either throw
or return a failed future, allowing you to handle errors at different levels. You may choose to handle errors inside your route handlers or by using middleware to catch the errors at a higher level, or both. You also need to deal with errors thrown inside the callbacks you provide to flatMap(_:)
and map(_:)
.
Dealing with errors in the callback
The callbacks for map(_:)
and flatMap(_:)
are both non-throwing. This presents a problem if you call a method inside the closure that throws. When returning a non-future type with a closure that needs to throw, map(_:)
has a throwing variant confusingly called flatMapThrowing(_:)
. To be clear, the callback for flatMapThrowing(_:)
returns a non-future type.
For example:
// 1
req.client.get("http://localhost:8080/users")
.flatMapThrowing { response in
// 2
let users = try response.content.decode([User].self)
// 3
return users[0]
}
Here’s what this example does:
- Make a request to an external API, which returns
EventLoopFuture<Response>
. You useflatMapThrowing(_:)
to provide a callback to the future that can throw an error. - Decode the response to
[User]
. This can throw an error, whichflatMapThrowing
converts into a failed future. - Return the first user — a non-future type.
Things are different when returning a future type in the callback. Consider the case where you need to decode a response and then return a future:
// 1
req.client.get("http://localhost:8080/users/1")
.flatMap { response in
do {
// 2
let user = try response.content.decode(User.self)
// 3
return user.save(on: req.db)
} catch {
// 4
return req.eventLoop.makeFailedFuture(error)
}
}
Here’s what’s happening:
- Get a user from the external API. Since the closure will return an
EventLoopFuture
, useflatMap(_:)
. - Decode the user from the response. As this throws an error, wrap this in
do
/catch
to catch the error - Save the user and return the
EventLoopFuture
. - Catch the error if one occurs. Return a failed future on the
EventLoop
.
Since the callback for flatMap(_:)
can’t throw, you must catch the error and return a failed future. The API is designed like this because returning something that can both throw synchronously and asynchronously is confusing to work with.
Dealing with future errors
Dealing with errors is a little different in an asynchronous world. You can’t use Swift’s do
/catch
as you don’t know when the promise will execute. SwiftNIO provides a number of methods to help handle these cases. At a basic level, you can chain whenFailure(_:)
to your future:
let futureResult = user.save(on: req.db)
futureResult.map {
print("User was saved")
}.whenFailure { error in
print("There was an error saving the user: \(error)")
}
If save(on:)
succeeds, the .map
block executes with the resolved value of the future as its parameter. If the future fails, it’ll execute the .whenFailure
block, passing in the Error
.
In Vapor, you must return something when handling requests, even if it’s a future. Using the above map
/whenFailure
method won’t stop the error happening, but it’ll allow you to see what the error is. If save(on:)
fails and you return futureResult
, the failure still propagates up the chain. In most circumstances, however, you want to try and rectify the issue.
SwiftNIO provides flatMapError(_:)
and flatMapErrorThrowing(_:)
to handle this type of failure. This allows you to handle the error and either fix it or throw a different error. For example:
// 1
return saveUser(on: req.db)
.flatMapErrorThrowing { error -> User in
// 2
print("Error saving the user: \(error)")
// 3
return User(name: "Default User")
}
Here’s what this does:
- Attempt to save the user. Use
flatMapErrorThrowing(_:)
to handle the error, if one occurs. The closure takes the error as the parameter and must return the type of the resolved future — in this caseUser
. - Log the error received.
- Create a default user to return.
Vapor also provides the related flatMapError(_:)
for when the associated closure returns a future:
return saveUser(on: req.db).flatMapError {
error -> EventLoopFuture<User> in
print("Error saving the user: \(error)")
return User(name: "Default User").save(on: req)
}
Since saveUser(on:)
returns a future, you must call flatMapError(_:)
instead. Note: The closure for flatMapError(_:)
cannot throw an error — you must catch the error and return a new failed future, similar to flatMap(_:)
above.
flatMapError
and flatMapErrorThrowing
only execute their closures on a failure. But what if you want both to handle errors and handle the success case? Simple! Simply chain to the appropriate method!
Chaining futures
Dealing with futures can sometimes seem overwhelming. It’s easy to end up with code that’s nested multiple levels deep.
Vapor allows you to chain futures together instead of nesting them. For example, consider a snippet that looks like the following:
return database
.getAllUsers()
.flatMap { users in
let user = users[0]
user.name = "Bob"
return user.save(on: req.db)
.map {
return .noContent
}
}
map(_:)
and flatMap(_:)
can be chained together to avoid nesting like this:
return database
.getAllUsers()
// 1
.flatMap { users in
let user = users[0]
user.name = "Bob"
return user.save(on: req.db)
// 2
}.map {
return .noContent
}
Changing the return type of flatMap(_:)
allows you to chain the map(_:)
, which receives the EventLoopFuture<Void>
. The final map(_:)
then returns the type you returned originally. Chaining futures allows you to reduce the nesting in your code and may make it easier to reason about, which is especially helpful in an asynchronous world. However, whether you nest or chain is completely personal preference.
Always
Sometimes you want to execute something no matter the outcome of a future. You may need to close connections, trigger a notification or just log that the future has executed. For this, use the always
callback.
For example:
// 1
let userResult: EventLoopFuture<Void> = user.save(on: req.db)
// 2
userResult.always {
// 3
print("User save has been attempted")
}
Here’s what this does:
- Save a user and save the result in
userResult
. This is of typeEventLoopFuture<Void>
. - Chain an
always
to the result. - Print a string when the app executes the future.
The always
closure gets executed no matter the result of the future, whether it fails or succeeds. It also has no effect on the future. You can combine this with other chains as well.
Waiting
In certain circumstances, you may want to actually wait for the result to return. To do this, use wait()
.
Note: There’s a large caveat around this: You can’t use
wait()
on the main event loop, which means all request handlers and most other circumstances.
However, as you’ll see in Chapter 11, “Testing”, this can be especially useful in tests, where writing asynchronous tests is difficult. For example:
let savedUser = try saveUser(on: database).wait()
Instead of savedUser
being an EventLoopFuture<User>
, because you use wait()
, savedUser
is a User
object. Be aware wait()
throws an error if executing the promise fails. It’s worth saying again: This can only be used off the main event loop!
SwiftNIO
Vapor is built on top of Apple’s SwiftNIO library (https://github.com/apple/swift-nio). SwiftNIO is a cross-platform, asynchronous networking library, like Java’s Netty. It’s open-source, just like Swift itself!
SwiftNIO handles all HTTP communications for Vapor. It’s the plumbing that allows Vapor to receive requests and send responses. SwiftNIO manages the connections and the transfer of data.
It also manages all the EventLoop
s for your futures that perform work and execute your promises. Each EventLoop
has its own thread.
Vapor manages all the interactions with NIO and provides a clean, Swifty API to use. Vapor is responsible for the higher-level aspects of a server, such as routing requests. It provides the features to build great server-side Swift applications. SwiftNIO provides a solid foundation to build on.
Where to go from here?
While it isn’t necessary to know all the details about how EventLoopFuture
s and EventLoop
s work under the hood, you can find more information in Vapor’s API documentation (https://api.vapor.codes/async-kit/main/AsyncKit/Extensions/EventLoopFuture.html) or SwiftNIO’s API documentation (https://apple.github.io/swift-nio/docs/current/NIO/Classes/EventLoopFuture.html). Vapor’s documentation site also has a large section (https://docs.vapor.codes/4.0/async/) on async and futures.