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.

5 (4) · 2 Reviews

Download materials
Save for later
Share

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.

Note: Full use of async/await requires Swift 5.5, Xcode 13.1 and macOS Monterey or greater.

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.

Overview of Trader's files in Xcode's file navigator.

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:

Xcode with the console opened and the Notice message indicating successful run of Trader.

Note: You can ignore the warning about no custom working directory, as it has no impact on this tutorial. If you’d like to resolve that warning anyway, read this section of Vapor’s official documentation.

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:

Calling an async function in a non-async function, resulting in Xcode errors about the limitation explained above.

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:

Calling an async function, in a Task(priority:operation:) which is in a non-async function. Results in no errors.

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 Tasks 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.