async/await in Server-Side Swift and Vapor
Learn how Swift’s new async/await functionality can be used to make your existing EventLoopFuture-based Vapor 4 code more concise and readable. By Mahdi Bahrami.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
async/await in Server-Side Swift and Vapor
25 mins
- Getting Started
- Bridging Between async/await and EventLoopFuture
- Converting EventLoopFuture to async/await
- Converting async/await to EventLoopFuture
- Understanding Asynchronous Tasks in Synchronous Contexts
- Leveraging async/await in Vapor
- Becoming More Fluent Than Ever!
- Updating Fluent Migrations
- Understanding Concurrent Loading
- Understanding Concurrent Loading with TaskGroup
- Understanding Concurrent Loading with async let
- Where to Go From Here?
In Server-Side Swift and Vapor, for the past several years EventLoopFuture
has been helping perform asynchronous tasks. While EventLoopFuture
is very powerful, it comes with a few disadvantages. First of all, it uses closures, which, over time, make it hard to write clean and readable code. Secondly, it has its own learning curve. Using async/await solves those two problems.
With async/await, your code will look like any other synchronous code: No closures and much nicer to read. Lots of people have already been using all the new async/await features in Vapor, and the community’s feedback so far has been immensely positive.
In this tutorial, you’ll migrate the sample project from using EventLoopFuture
to async/await. The sample project:
- Implements an API for trading crates
- Uses Fluent to store that information in the database
- Adds routes that will be migrated to async/await
This tutorial assumes you’re comfortable building simple Vapor 4 apps. If you’re new to Vapor, check out Getting Started with Server-Side Swift with Vapor 4.
You’ll also use Fluent to interact with a PostgreSQL database. If you’re unfamiliar with Fluent and running a database on Docker, check out Using Fluent and Persisting Models in Vapor.
This tutorial assumes you’re comfortable building simple Vapor 4 apps. If you’re new to Vapor, check out Getting Started with Server-Side Swift with Vapor 4.
You’ll also use Fluent to interact with a PostgreSQL database. If you’re unfamiliar with Fluent and running a database on Docker, check out Using Fluent and Persisting Models in Vapor.
Getting Started
Download the sample project by clicking Download Materials at the top or bottom of this tutorial. The starter project is a simple API that tracks “crates” being traded between different owners. You will change it to use async/await.
Open the starter project in Xcode. You’ll see it contains a variety of files and folders.
First things first! Open Package.swift
. Since you’re using async/await, you need to change a few lines.
The very first line declares the Swift version that your app uses. Make sure it’s at least 5.5:
// swift-tools-version:5.5
Scroll down a bit. In the Package
declaration, change Trader’s macOS platform to version 12:
platforms: [
.macOS(.v12)
],
Finally, in targets
, declare the Run
target as an executableTarget
instead of a normal target
:
.executableTarget(name: "Run", dependencies: [.target(name: "App")]),
This is required for executable targets in Swift 5.5. While you’re waiting for Xcode to resolve Trader’s dependencies, open Terminal. Copy and paste the following into your Terminal window to get your PostgreSQL database going with the help of Docker:
docker run --name traderdb -e POSTGRES_DB=vapor_database \
-e POSTGRES_USER=vapor_username \
-e POSTGRES_PASSWORD=vapor_password \
-p 5432:5432 -d postgres
This creates a PostgreSQL database running in Docker called `traderdb`. Now, go back to Xcode. Build and run Trader using the shortcut Command-R or the top-left Play button. Wait for Xcode to run Trader. Then, make sure the console is open. You’ll see a NOTICE
indicating Trader’s successful run on address http://127.0.0.1:8080
:
That’s it for now! Time to learn about the foundation of Vapor’s async/await support.
Bridging Between async/await and EventLoopFuture
Everything you’ll learn in the following sections is implemented using two simple but powerful tools. These enable developers to migrate their code to async/await without much trouble, but you’ll learn that you have even better options most of the time.
Converting EventLoopFuture to async/await
The first tool is a useful get()
function on top of EventLoopFuture
, which enables retrieving the inner value of any EventLoopFuture
using the async/await syntax. Consider this code:
let usernameFuture: EventLoopFuture<String> = getUsernameFromDatabase()
You can asynchronously retrieve this function’s value using the new async/await syntax. Notice get()
being called:
let username: String = try await usernameFuture.get()
As you can see, get()
easily converts your EventLoopFuture<String>
to a simple String
.
Converting async/await to EventLoopFuture
What if you need the exact opposite of what the get()
function does? Sometimes, you don’t have control over a piece of your existing code, or you might want to postpone its migration until another time. At the same time, you still need it to work with other parts of your code that are using async/await.
That’s when the second tool comes in handy. Imagine you want to convert the async/await code below to something that returns an EventLoopFuture
:
let email: String = try await getUserEmailAsync()
In Vapor, assuming you have access to an EventLoop
, you can simply do the following:
let emailFuture: EventLoopFuture<String> = eventLoop.performWithTask {
return try await getUserEmailAsync()
}
In the code above, first, you simply call the performWithTask(_:)
function, which is available on any EventLoop
. Then, you perform your async
work in its closure, and at last, you return the result of your asynchronous work.
That’s as easy as it gets, but the better news is that the majority of Vapor’s core packages have already been updated with async/await support. Most of the time, you don’t even need to use either of those two functions! :]
Understanding Asynchronous Tasks in Synchronous Contexts
All async/await functions are only callable in an asynchronous context. This means you can’t await
any async
work in a non-async
function:
But sometimes you need to bypass this limitation. That’s when Task
comes in. In Swift, a Task
is a piece of asynchronous work and can be started from anywhere. You can simply initialize a new instance of Task
and perform your asynchronous work there:
As you can see, Task
also takes in a priority
argument. You can specify the priority of the asynchronous task you want to be done, so Swift executes your asynchronous work based on that.
The preference is not to use Task
s yourself because SwiftNIO will have less control over your asynchronous operations in the future. As of Swift 5.5, Swift’s own system manages all async/await works, but SwiftNIO will take over this role in the near future. That’s when you’ll appreciate yourself for not creating new instances of Task
everywhere! :]
The fact that, for now, SwiftNIO doesn’t have full control over the execution of async
operations has a disadvantage: You can expect your async/await code to be slightly slower than your EventLoopFuture
code.