Async/await in Vapor

Swift 5.5 with the new async/await features is going to be released quite soon, and I’m very excited. And while all the new async APIs in UIKit and SwiftUI are neat, they don’t actually catch my eye all that much. As of now, async/await will only be available on the fall-2021 Apple OS releases, and following the general pattern of “current iOS minus one” that means async/await will be available starting… fall of 2022.

However, that’s not the only place I use Swift! I also dabble in server-side Swift when I can, and over there, it’s ready-ish to use now.1

And hey, would you look at that, Vapor’s already got a PR open for adding async variants of the core APIs. Neat! Let’s see how this is implemented.

And, hey, it’s… pretty simple, really! All it takes is getting into the (pre-release!) _NIOConcurrency module, and suddenly any EventLoopFuture has a new function: public func get() async throws -> T. The implementation of that is also lovely and simple, thanks to Swift’s withUnsafeThrowingContinuation and the way SwiftNIO already works.

Now, where I’m more excited about this than Vapor is in Fluent — a lot of the server-side work I do is in C#, using the excellent Entity Framework Core. Database requests are straightforward: MyModel model = await db.MyModels.FirstOrDefaultAsync(m => m.id == id);

With the non-async version of Vapor and Fluent, this is… a bit more difficult to work with. MyModel.find(id, on: req.db).flatMap { model in ... }

At first glance it doesn’t look too bad, but notice the flatMap — we can’t just continue writing our code, we have to move into a new scope. Hello, pyramid of doom.

But now?2 Now, we can await things. Fluent looks more like Entity Framework all of a sudden: let model = try await MyModel.find(id, on: req.db)

That’s already a much nicer API to work with, and it delivers on the promise of async/await: much simpler flow of code. I’m excited to untangle some of my more complex flatMap pyramids.

What I’m still curious about, and will need to do some testing with, is to see how well it does with concurrency. It’s a known issue in Entity Framework that you can’t run multiple requests against a single DbContext; what I’m wondering is if Fluent has this same limitation, or if I can safely do something like:

async let model1 = try MyModel.find(id1, on: req.db)
async let model2 = try MyModel.find(id2, on: req.db)
let result = MyResult(model1: await model1, model2: await model2)

That, I don’t yet know! I’ll have to do some experimenting and try it — which may be a future blog post. Still, even if I have to immediately await everything, the improvements to my code’s legibility will be worth it.

Disclaimer: Swift 5.5 is, as of this writing, pre-release; similarly, the versions of Swift-NIO and Vapor that provide async APIs are pre-release. Don’t use these in production. ↩

Well, soon: this as another draft PR as of this writing. ↩