I always imagined that initializers were synchronous in nature. You may fetch the data required by the initializer asynchronously and pass it to the initializer, but the initializer itself is synchronous.
But with Swift 5.5, we can now have asynchronous initializers. This means that the initialization of an instance can be awaited, and the initializer can perform asynchronous operations by itself, before returning the instance.
Why is this important?
Consider a scenario where you need to fetch some data from a remote server before initializing an instance. You can now do this in the initializer itself.
How to create an asynchronous initializer?
Similar to asynchronous functions, an initializer can be marked with the async
keyword. In the very basic form, it would look like this:
struct SomeStruct {
init() async {
// Perform asynchronous operations
}
}
When using an asynchronous initializer, the initializer must be called with await
keyword, which marks a possible suspension point in the code.
let instance = await SomeStruct()
At this point, the execution of the code may be suspended until all of the asynchronous operations in the initializer are completed. After which the instance will be returned.
Inherited Capabilities
One thing to note here is that the async initializers still keep all of their capabilities of a regular initializer.
- Async initializers can have access control modifiers.
- They can accept arguments and have argument labels.
- They can have default values for the arguments.
- They can call other initializers, both async and sync. Note that sync initializers however cannot call async initializers.
- They can throw errors.
- They can be optional.
- In case of a class type, they can be marked as
convenience
orrequired
. - In case of a class type, they can call initializers from the superclass.
- In case of a class type, they can be overridden in subclasses.
Asynchronous Throwing Initializers
Asynchronous initializers can also throw errors. If an error is thrown, the initializer will not return an instance. Call to a throwing asynchronous initializer must be preceded by try await
.
class User {
let id: Int
let name: String
init(id: Int) async throws {
self.id = id
self.name = try await fetchUserNameAsynchronously(id: id)
}
}
// Usage
do {
let user1 = try await User(id: 1)
print(user1.name) // John Doe
let user2 = try await User(id: 2)
print(user2.name)
} catch {
print(error) // notFound
}
// Mock implementation
enum UserError: Error {
case notFound
}
func fetchUserNameAsynchronously(id: Int) async throws -> String {
try? await Task.sleep(nanoseconds: 1_000_000_000)
if id == 1 {
return "John Doe"
} else {
throw UserError.notFound
}
}