Home Server-Side Swift Tutorials

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.

Version

  • Swift 5.5, macOS 12, Xcode 13

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
Note: Full use of async/await requires Swift 5.5, Xcode 13.1 and macOS Monterey or greater.

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.

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.

Leveraging async/await in Vapor

Let’s go back to our Trader project. Open CrateController.swift in the Controllers folder. You’ll see boot(routes:) in CrateController is registering some routes:

func boot(routes: RoutesBuilder) throws {
    let crateGroup = routes.grouped("crates")
    // 1
    crateGroup.get(use: all)
    // 2
    crateGroup.post(use: create)
    // 3
    crateGroup.post("trade", use: trade)
}

In the code above, three routes are registered:

  1. GET HTTP, at /crates endpoint to retrieve all current crates.
  2. POST HTTP, at /crates endpoint to add new crates.
  3. POST HTTP, at /crates/trade endpoint to enable trading of crates.

Look at all(req:), and try to convert it to an async/await function:

private func all(req: Request) throws -> EventLoopFuture<[Crate]> {
    Crate.query(on: req.db).all()
}

Based on what you previously learned, you can append .get() to the crates query, and it will work with async/await. Don’t forget to change the function’s signature as well. The function becomes an async throwing function that returns [Crate], instead of just a normal throwing function that returns an EventLoopFuture<[Crate]>:

private func all(req: Request) async throws -> [Crate] {
    try await Crate.query(on: req.db).all().get()
}

Build and run Trader again, and you won’t see any compile errors. Your first conversion of an EventLoopFuture function to an async/await function is successful!

But, that’s not all. Remember earlier you saw that the two helpers won’t be needed in most cases. So remove the call to get():

try await Crate.query(on: req.db).all()

Build and run again. You’ll see that Xcode successfully runs your app and doesn’t complain about all() not being an async function. How is that happening?

To make using async/await cleaner and nicer, the Vapor team has added a secondary async function for the most popular functions that return an EventLoopFuture. That means, in this case, you have access to two all() functions. The classic one returns an EventLoopFuture, and the modern one is an async function. In the code above, you’re using the async/await variant of all(), and Swift identifies that with no problems.

The same is true about crateGroup.get(use: all). You’ve changed all(req:) from returning an EventLoopFuture to returning an async value. But the Vapor team has already added a secondary async function overload for all route builders. So, here Swift automatically uses the async/await variant of get(use:) and doesn’t complain at all.

This is the whole idea behind the current async/await support in Vapor. The majority of the functions that were using EventLoopFuture now have an async/await version, so you can simply use them and enjoy!

Now, it’s time to convert create(req:) to async/await. create(req:) creates new crates based on the payload you send it and adds them to the database. It currently looks like this:

private func create(req: Request) throws -> EventLoopFuture<[Crate]> {
    let crates = try req.content.decode([Crate].self)
    return crates.create(on: req.db).transform(to: crates)
}

As you learned, there are two simple steps:

  • Change the function’s signature from -> EventLoopFuture<Value> to async throws -> Value.
  • Change the code inside the function to use the async/await variants.

So, after the conversion, this is your function:

private func create(req: Request) async throws -> [Crate] {
    // 1
    let crates = try req.content.decode([Crate].self)
    // 2
    try await crates.create(on: req.db)
    // 3
    return crates
}

Here’s what you’re doing:

  1. You decode the payload that’s sent to you.
  2. Now, you save the decoded crates in the PostgreSQL database.
  3. Finally, you return the crates as the response.

So far, you’ve learned all the basics of using async/await in Vapor. You’ll get back to CrateController later to learn some more advanced stuff about async/await.

Becoming More Fluent Than Ever!

Fluent is one of Vapor’s core packages, and they’ve updated it with async/await support. This means you can await any queries that you’d ever want to make.

You’ve already tried a few different Fluent queries in all(req:) and create(req:) functions. Now, it’s time to see how to use async/await for a Fluent migration.

Updating Fluent Migrations

Using async/await in Fluent migrations is just as easy, with only one difference from the previous sections. Open CreateCrate.swift in the Migrations folder. You’ll see the following migration:

struct CreateCrate: Migration {
    func prepare(on database: Database) -> EventLoopFuture<Void> {
        database
            .schema(Crate.schema)
            .id()
            .field(Crate.FieldKeys.owner, .string, .required)
            .field(Crate.FieldKeys.item, .string, .required)
            .create()
    }
    
    func revert(on database: Database) -> EventLoopFuture<Void> {
        database
            .schema(Crate.schema)
            .delete()
    }
}

By now, you should be able to update the two functions easily on your own. At the end, you’ll have:

struct CreateCrate: Migration {
    func prepare(on database: Database) async throws {
        try await database
            .schema(Crate.schema)
            .id()
            .field(Crate.FieldKeys.owner, .string, .required)
            .field(Crate.FieldKeys.item, .string, .required)
            .create()
    }
    
    func revert(on database: Database) async throws {
        try await database
            .schema(Crate.schema)
            .delete()
    }
}

Try to build and run Trader. Xcode throws an error that CreateCrate doesn’t conform to Migration:

Xcode showing build error indicating CreateCrate doesn't conform to Migration.

Why is this happening? The reason is very simple. Migration expects prepare(on:) -> EventLoopFuture<Void> and revert(on:) -> EventLoopFuture<Void>, but you’ve just changed those functions to simply be async throws.

The Vapor team has come up with a simple solution. They’ve added the AsyncMigration protocol. It’s the same as the Migration protocol, but accepts async prepare(on:) and revert(on:) functions.

Now, replace Migration with AsyncMigration:

struct CreateCrate: AsyncMigration {
...

AsyncMigration itself conforms to Migration under the hood, so CreateCrate still conforms to Migration. This means you can use CreateCrate as if nothing has changed, and you won’t need to change any more code anywhere else!

Build and run Trader. You’ll see that everything compiles and Trader runs successfully.

Generally speaking, if you were previously using a protocol/type that uses EventLoopFuture, you can expect an async/await variant of it. Usually, you can identify the async/await variants using the Async prefix that they have. For example, in this case, AsyncMigration is the async/await variant of Migration, so it has an Async prefix.

Understanding Concurrent Loading

Now, it’s time to migrate your remaining routes to async/await. Your current knowledge will lead you to a suboptimal conversion.

Open CrateController.swift and look at tradeOne(on:tradingSides:):

private func tradeOne(
    on db: Database,
    tradingSides: TradeItem
) -> EventLoopFuture<HTTPStatus> {
    // 1
    Crate.query(on: db)
        .filter(\.$id ~~ [tradingSides.firstId, tradingSides.secondId])
        .all()
        .tryFlatMap { bothCrates in
            // 2
            guard bothCrates.count == 2 else {
                throw Abort(.badRequest)
            }
            let crate1 = bothCrates[0]
            let crate2 = bothCrates[1]
            // 3
            (crate1.owner, crate2.owner) = (crate2.owner, crate1.owner)
            // 4
            let saveCrate1 = crate1.save(on: db)
            let saveCrate2 = crate2.save(on: db)
            // 5
            return saveCrate1.and(saveCrate2).transform(to: .ok)
        }
}

In the code above:

  1. You retrieve the current crates for each side of the trade. Remember, TradeItem is only a container for ids of two crates, whose items will be traded by the app. The query finds all crates that have either of the crate ids.
  2. You make sure you have two crates retrieved. Then, assign each crate to a variable.
  3. Afterward, you swap the items of the crates. crate1 will have crate2‘s item, and vice versa. This is where the trading happens.
  4. Then, you save the new crates in the database.
  5. Finally, you return the result of the saving process and transform it to a simple 200 OK HTTP status.

Now, you’ll transform tradeOne(on:tradingSides:) to an async/await function. By the end of the process, you should have code similar to this:

private func tradeOne(
    on db: Database,
    tradingSides: TradeItem
) async throws -> HTTPStatus {
    // 1
    let bothCrates = try await Crate.query(on: db)
        .filter(\.$id ~~ [tradingSides.firstId, tradingSides.secondId])
        .all()
    // 2
    guard bothCrates.count == 2 else {
        throw Abort(.badRequest)
    }
    let crate1 = bothCrates[0]
    let crate2 = bothCrates[1]
    // 3
    (crate1.owner, crate2.owner) = (crate2.owner, crate1.owner)
    // 4
    try await crate1.save(on: db)
    try await crate2.save(on: db)
    // 5
    return .ok
}

Here, you:

  1. Retrieve the current crates for each side of the trade.
  2. Make sure you’ve retrieved two crates. Then, assign each crate to a variable.
  3. Swap the items of the crates. crate1 will have crate2‘s item, and vice versa. This is where the trading happens.
  4. Save the new crates in the database.
  5. Return a 200 OK HTTP status code.

Build and run, and you might see some errors indicating that trade(req:) is unhappy with tradeOne(on:tradingSides:). That’s normal, as you just migrated tradeOne(on:tradingSides:) to async/await without changing how the function is called in trade(req:).

So, you need to convert trade(req:) to async/await. Right now, you have:

private func trade(req: Request) throws -> EventLoopFuture<HTTPStatus> {
    // 1
    let allTrades = try req.content.decode([TradeItem].self)
    // 2
    return allTrades.map { tradingSides in
        tradeOne(on: req.db, tradingSides: tradingSides)
    }
    // 3
    .flatten(on: req.eventLoop)
    // 4
    .transform(to: .ok)
}

Here’s what this code does:

  1. Decodes a TradeItem array from the body of the request sent to you.
  2. Iterates through allTrades and performs trades for each TradeItem using tradeOne(on:tradingSides:).
  3. Now that you have an array of EventLoopFuture<Void>, you flatten it to make it one EventLoopFuture<[Void]>, which contains the results of all trades.
  4. Finally, you transform the result of the tasks to a 200 OK HTTP status.

Try to migrate this function to async/await. By the end of the conversion, you’ll have something like this:

private func trade(req: Request) async throws -> HTTPStatus {
    // 1
    let allTrades = try req.content.decode([TradeItem].self)
    // 2
    for tradingSides in allTrades {
        _ = try await tradeOne(on: req.db, tradingSides: tradingSides)
    }
    // 3
    return .ok
}

In the code above, you:

  1. Decode an array of TradeItem from the body of the request sent to you.
  2. Iterate through allTrades and perform the trades for each TradeItem using tradeOne(on:tradingSides:). You don’t need the final results of tradeOne(on:tradingSides:) calls — all that’s important is that the process doesn’t throw any errors.
  3. Then, return a 200 OK HTTP status.

Do the async/await conversions of trade(req:) and tradeOne(on:tradingSides:) look good to you? You might not have noticed, but this async/await code has some differences from the original EventLoopFuture code in terms of execution.

Understanding Concurrent Loading with TaskGroup

Look at trade(req:) again, and recheck how these two codes compare to each other.

This is your old EventLoopFuture code:

allTrades.map { tradingSides in
    tradeOne(on: req.db, tradingSides: tradingSides)
}
.flatten(on: req.eventLoop)

And this is your new async/await code:

for tradingSides in allTrades {
    _ = try await tradeOne(on: req.db, tradingSides: tradingSides)
}

The problem is that all trades in the EventLoopFuture version of your code are running simultaneously. However, in the async/await version, Swift executes them one after another. With EventLoopFuture, every task starts possibly as soon as an EventLoopFuture is created. But with the async/await version of this code, every time Swift hits an await in the loop, it’ll stop and wait for the result of the task. This makes your code’s async/await variant slower when multiple trades happen.

The solution is to use a TaskGroup. TaskGroup is a group of tasks that are done concurrently. To get access to a TaskGroup, you need to use either withTaskGroup(of:returning:body:) or withThrowingTaskGroup(of:returning:body:). The difference between those two functions is that the second function gives you access to a ThrowingTaskGroup whose addTask(priority:operation:) accepts throwing operations. ThrowingTaskGroup, as its name suggests, is just a throw-friendly version of a TaskGroup.

A better conversion for trade(req:) is:

private func trade(req: Request) async throws -> HTTPStatus {
    // 1
    let allTrades = try req.content.decode([TradeItem].self)
    // 2
    return try await withThrowingTaskGroup(
        of: HTTPStatus.self
    ) { taskGroup in
        // 3
        for tradingSides in allTrades {
            taskGroup.addTask {
                try await tradeOne(on: req.db, tradingSides: tradingSides)
            }
        }
        // 4
        try await taskGroup.waitForAll()
        // 5
        return .ok
    }
}

Here, you:

  1. Decode an array of TradeItems from the body of the request sent to you.
  2. Use withThrowingTaskGroup(of:returning:body:) to make a new TaskGroup. The of argument defines the type of value that you’ll return when using the addTask(priority:operation:) function. Note that you see no returning argument because Swift can automatically infer that this TaskGroup will return an HTTPStatus.
  3. Iterate through the tasks and use the addTask(priority:operation:) to add a new task to the TaskGroup for each tradingSides. For the priority argument, you can use the default value.
  4. Wait for all tasks to finish. An important note is that even without waitForAll(), Swift returns from the closure only when all tasks are done. Use of waitForAll() here is to make sure all thrown errors are caught. Without it, even if one tradeOne(on:tradingSides:) throws an error, you wouldn’t be notified of it.
  5. Return a 200 OK HTTP status.

Note that you can iterate through each value in a TaskGroup and capture results of each task. You can either use the next() function on a TaskGroup, or use a loop like so:

for await taskResult in taskGroup {
    // do something with the `taskResult`
}

Luckily, you don’t need the results of the trades here, so you don’t need to capture any of the results.

Understanding Concurrent Loading with async let

Now that you’ve fixed the trade(req:) function, look at tradeOne(on:tradingSides:). It contains these two lines:

try await crate1.save(on: db)
try await crate2.save(on: db)

That’s another piece of suboptimal code! Trader waits once for each crate’s save operation, even though the operations aren’t dependent on each other. To solve that, you can still use a TaskGroup, but that would be overkill. The better way is replacing those two lines with the code below:

async let crate1Saving: Void = crate1.save(on: db)
async let crate2Saving: Void = crate2.save(on: db)
_ = try await (crate1Saving, crate2Saving)

You should’ve noticed that although save(on:) performs an async work, there’s no await keyword behind it when assigning it to an async let. async variables are a new addition to Swift. They simply allow assigning an async function’s value to a variable without actually awaiting the asynchronous operation at that point. This is the exact thing happening in the code above. You’re assigning two save operations’ values to crate1Saving and crate2Saving, but you’re postponing the await process to be done somewhere else at another time.

The last line is what awaits on both processes to complete before continuing. There, you assign the save processes to a tuple by declaring (crate1Saving, crate2Saving). Then, you use try await to await both of them simultaneously. The _ = part of the code is there because you don’t need the results of the operations.

For your use cases, TaskGroup and async let were the best solutions, but you can also use the Task.detached(priority:operation:) function to run tasks concurrently. Learning about that will have to wait for another time. :]

Where to Go From Here?

You can download the sample project by clicking Download Materials at the top or bottom of this tutorial.

In this tutorial, you learned about the most important Vapor-related concurrency features, but there’s still a lot left to learn.

To learn about all the new concurrency features, check out the book Modern Concurrency in Swift.

For more about Vapor’s new async/await APIs, see the official Vapor documentation.

We hope you enjoyed this tutorial, and if you have any questions or comments, please join the forum discussion below!

Reviews

More like this

Contributors

Comments