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?
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:
-
GET
HTTP, at /crates endpoint to retrieve all current crates. -
POST
HTTP, at /crates endpoint to add new crates. -
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>
toasync 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:
- You decode the payload that’s sent to you.
- Now, you save the decoded crates in the PostgreSQL database.
- 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
:
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:
- You retrieve the current crates for each side of the trade. Remember,
TradeItem
is only a container forid
s of two crates, whose items will be traded by the app. The query finds all crates that have either of the crateid
s. - You make sure you have two crates retrieved. Then, assign each crate to a variable.
- Afterward, you swap the items of the crates.
crate1
will havecrate2
‘s item, and vice versa. This is where the trading happens. - Then, you save the new crates in the database.
- 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:
- Retrieve the current crates for each side of the trade.
- Make sure you’ve retrieved two crates. Then, assign each crate to a variable.
- Swap the items of the crates.
crate1
will havecrate2
‘s item, and vice versa. This is where the trading happens. - Save the new crates in the database.
- 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:
- Decodes a
TradeItem
array from the body of the request sent to you. - Iterates through
allTrades
and performs trades for eachTradeItem
usingtradeOne(on:tradingSides:)
. - Now that you have an array of
EventLoopFuture<Void>
, you flatten it to make it oneEventLoopFuture<[Void]>
, which contains the results of all trades. - 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:
- Decode an array of
TradeItem
from the body of the request sent to you. - Iterate through
allTrades
and perform the trades for eachTradeItem
usingtradeOne(on:tradingSides:)
. You don’t need the final results oftradeOne(on:tradingSides:)
calls — all that’s important is that the process doesn’t throw any errors. - 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.