Skip to content

Convert await -> Result<T, Error> functions to await throws -> T #8104

@mokagio

Description

@mokagio

I recently submitted a PR to address a warning. @jkmassel spotted the method I operated in has signature await -> Result<Void, Error>, at which he commented "the use of async -> Result is unfortunate here".

I think what Jeremy is hinting at is that, while evidently compiling correctly, async and Result are somewhat clashing from a functionality point of view.

Background on Result and asycn/await

Prior to async/await, it was useful to use Result in asynchronous method to have a single callback that safely encapsulated both success and failure outcomes:

func someAsyncComputation(_ onCompletion: Result<Int, Error>)

With async/await, we have an alternative pattern available

func someComputation() async throws -> Int

Of course, the option used in the codebase also compiles, but it feels a bit stuck in the middle.

func someComputation() async -> Result<Int, Error>

Here's the difference at call site:

// Result in closure
someAsyncComputation { result in 
  switch result {
  case .success(let value): ...
  case .failure(let error): ...
  }
}

// async throws
do {
  let value = try await someComputation()
  // ...
} catch {
  // ...
}

// async throws in a function that itself throws to bubble up the error
let value = try await someComputation

// async Result
let result = await someComputation()
switch result  {
  case .success(let value): ...
  case .failure(let error): ...
}

Personally, I am a big fan of Result and I love seeing it in use. But I think it works best when chaining functional programming style transformations in railway oriented programming:

result
  .map { ... }
  .flatMap { ... }
  .mapError { ... }
  // etc

Where to go from here

At the time of writing, there are 24 matches for async -> Result in the codebase.

image

I sampled them and I don't see us leveraging Result in a railway oriented programming way. Example see loadUsernameSuggestions(from:) defined as

func loadUsernameSuggestions(from text: String) async -> Result<[String], Error>

and used as

let usernameSuggestionsResult = await remote.loadUsernameSuggestions(from: base)
guard case let .success(usernameSuggestions) = usernameSuggestionsResult,
let username = usernameSuggestions.first else {
return nil
}
return username

We could rewrite this to something like

return try? await remote.loadUsernameSuggestions(from: base)?.first

It's also possible we'll find some calls that ought to remain callback-based and could benefit with Result without async.


I'd love to hear your thoughts on this. Thanks 😄

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions