Swift 6.4 adds async support to Result


Greetings, traveler!

Swift has had Result.init(catching:) for a long time. It is a small convenience initializer that runs a throwing closure and stores the outcome as a value. If the closure returns successfully, you get .success. If it throws, you get .failure.

That worked nicely for synchronous code.

let result = Result {
    try decodeUser(from: data)
}

But as soon as the operation became async, the convenience disappeared.

The old problem

Before Swift 6.4, Result.init(catching:) could not wrap an async throws operation.

So if you wanted to convert an async throwing call into a Result, you usually had to write this:

let result: Result<Data, Error>

do {
    let data = try await fetchRemoteData()
    result = .success(data)
} catch {
    result = .failure(error)
}

This is not difficult code. But it is boilerplate.

And it tends to appear in the same places: view models, compatibility layers, callback bridges, reducers, and APIs that store loading state as a value.

Some teams solved it by adding their own helper extension. Swift 6.4 makes that helper part of the standard library.

The new initializer

Swift 6.4 adds an async overload of Result.init(catching:).

Now you can write:

let result: Result<Data, Error> = await Result {
    try await fetchRemoteData()
}

The important detail is the await before Result.

The initializer itself is async because it has to run an async closure. After the line finishes, result already contains the completed outcome. It is not a task. It is not lazy. It does not store future work.

It simply runs the operation and captures the result.

What the code means

This code:

let result: Result<Data, Error> = await Result {
    try await fetchRemoteData()
}

is equivalent to:

let result: Result<Data, Error>

do {
    let data = try await fetchRemoteData()
    result = .success(data)
} catch {
    result = .failure(error)
}

So the new API does not introduce a new error handling model.

It only removes the manual do/catch when your goal is already to produce a Result.

async throws is still the better shape for many APIs. Result is useful when you need to keep success or failure as data.

A common iOS use case

One place where this fits well is loading state.

enum LoadingState<Value> {
    case idle
    case loading
    case loaded(Result<Value, Error>)
}

@MainActor
final class ProfileViewModel: ObservableObject {
    @Published private(set) var state: LoadingState<Profile> = .idle

    private let service: ProfileService

    init(service: ProfileService) {
        self.service = service
    }

    func loadProfile() async {
        state = .loading

        let result: Result<Profile, Error> = await Result {
            try await service.fetchProfile()
        }

        state = .loaded(result)
    }
}

Here Result is not used instead of async throws. The service can still expose a clean async throwing API.

func fetchProfile() async throws -> Profile

The view model converts that operation into a value because the UI state wants to store both outcomes.

Bridging to callback-based APIs

Another useful case is bridging modern async code to an older result-based callback.

func loadAvatar(completion: @escaping (Result<Avatar, Error>) -> Void) {
    Task {
        let result: Result<Avatar, Error> = await Result {
            try await avatarService.loadAvatar()
        }

        completion(result)
    }
}

This kind of code appears during migrations.

Maybe most of the app already uses async/await, but one module still expects a completion handler. The new initializer keeps the bridge small and readable.

Typed throws support

The proposed API also fits typed throws. For example, an async function can throw a specific error type:

enum ProfileError: Error {
    case unauthorized
    case notFound
}

func fetchProfile() async throws(ProfileError) -> Profile {
    // Fetch profile
}

Then Result can preserve that failure type:

let result: Result<Profile, ProfileError> = await Result {
    try await fetchProfile()
}

If your API already models errors precisely, the conversion to Result does not have to erase them to Error.

Be careful with cancellation

Result.init(catching:) catches thrown errors and stores them as .failure.

So if the async operation throws CancellationError, that cancellation can become a failure value.

let result: Result<Data, Error> = await Result {
    try await fetchRemoteData()
}

Sometimes that is exactly what you want. For example, your UI state may need to say that the operation ended without producing data.

But in other flows, cancellation should stop the current task rather than become a value that continues through the system.

In those cases, plain try await is still clearer.

When not to use it

The new initializer is useful, but it is not something to put around every async call.

This is probably unnecessary:

let result: Result<Profile, Error> = await Result {
    try await service.fetchProfile()
}

let profile = try result.get()

If you immediately unwrap the Result, the extra conversion does not buy much.

This is simpler:

let profile = try await service.fetchProfile()

Use Result when the result itself is the thing you need to pass, store, compare, or expose.

Final thought

Most of the time, async throws should stay as async throws. But when you need to turn an async operation into a value, Swift 6.4 finally gives Result the initializer it was missing.