Articles, podcasts and news about Swift development, by John Sundell.

Throwing and asynchronous Swift properties

Published on 27 Jul 2021
Basics article available: Error Handling

Swift 5.5 introduces a new concept called “effectful read-only properties”, which essentially means that computed properties can now utilize control flow mechanisms like errors and async operations when computing their values.

RevenueCat

RevenueCat: Easily build and manage iOS and Android in-app purchases. With just a few lines of code RevenueCat provides IAP infrastructure, customer analytics, data integrations, and gives you time back from dealing with edge cases and updates across all platforms.

Throwing properties

Let’s start by taking a look at how computed properties can now throw errors using Swift’s standard error handling mechanism. As an example, let’s say that we’re currently using the built-in Result type’s get method to either extract a result’s wrapped value, or throw any error that it contains:

func handleLoginResult(_ result: Result<User, Error>) throws {
    let user = try result.get()
    ...
}

Since that get method doesn’t actually perform any kind of work, but rather just lets us unpack a Result value using the try keyword, it could now just as well be declared as a property — for example like this:

extension Result {
    var value: Success {
        get throws { try get() }
    }
}

With the above in place, we can now retrieve the underlying value from any Result instance simply by accessing our new value property, as long as we prefix those expressions with try and handle any errors that might be thrown — just like when using the get method directly:

func handleLoginResult(_ result: Result<User, Error>) throws {
    let user = try result.value
    ...
}

While the fact that computed properties can now throw errors could become really useful in certain situations, it’s important to still keep the semantic differences between properties and functions in mind. So while the above value property might make complete sense as a property (as it just retrieves an underlying value), many throwing operations would arguably still be better implemented as functions. To learn more about my thoughts on that topic, check out “Computed properties in Swift”.

Asynchronous properties

Along with its new concurrency system, Swift 5.5 also enables computed properties to be completely asynchronous. Similar to how properties can now use the throws keyword, any property annotated with async can now freely call other asynchronous APIs, and the code that accesses such a property will be suspended by the system until all of those underlying asynchronous operations have been completed.

For example, here a DatabaseEntity asynchronously checks with its parent Database if it has been synced whenever its isSynced property is accessed:

class DatabaseEntity {
    var isSynced: Bool {
        get async {
    await database?.isEntitySynced(self) ?? false
}
    }

    private weak var database: Database?
    ...
}

When it comes to asynchronous code, we arguably have to be even more careful as to what sort of operations that we implement behind a property-based API. For example, we probably don’t want to perform tasks like network calls or other relatively long-running operations as part of computing a property, but for things that require jumping to a separate dispatch queue, or tasks that involve some form of file I/O, async-marked properties could prove to be quite useful.

Protocol requirements

Finally, let’s also take a quick look at what these new “effectful properties” look like when declared as part of a protocol. Just like how a protocol can define standard properties as part of its list of requirements, a protocol can now also require those properties to be async, and can optionally enable them to throw.

For example, here’s what our above isSynced property might look like if we were to define it as part of a protocol:

protocol Syncable {
    var isSynced: Bool { get async }
}

If we then also wanted to enable types conforming to Syncable to throw errors as part of their isSynced implementation, then we could simply add the throws keyword to the above declaration — like this:

protocol Syncable {
    var isSynced: Bool { get async throws }
}

Just like other throwing protocol requirements, implementations of a throws-marked computed property doesn’t actually have to throw, they just have the option to do so.

Support Swift by Sundell by checking out this sponsor:

RevenueCat

RevenueCat: Easily build and manage iOS and Android in-app purchases. With just a few lines of code RevenueCat provides IAP infrastructure, customer analytics, data integrations, and gives you time back from dealing with edge cases and updates across all platforms.

Conclusion

So that’s a quick look at how computed properties can now be marked with either the throws or async keyword, starting in Swift 5.5. I hope you enjoyed this article, and feel free to let me know if you have any questions, comments, or feedback — either via Twitter or email.

Thanks for reading!