Throwing and asynchronous Swift properties
Discover page available: ConcurrencySwift 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.
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.
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!