Using Result type with async/await

You could create an extension on the Result type to preserve the type of the failure

extension Result {
    func getFailure() -> Failure? {
        if case .failure(let failure) = self {
            return failure
        }
        return nil
    }
}
let result = await fetchProducts()
if let error = result.getFailure() {
    handleError(error)
    return
}
let products = try! result.get()
// use products...

Though some people don't like to use try!

This is what Result.get is for:

do {
    let products = try await fetchProducts().get()
    let customers = try await fetchCustomers(products: products).get()
    let orders = try await fetchOrders(customers: customers).get()
    print("orders: \(orders)")

} catch {
    print("error: \(error)")
}

func fetchProducts() async -> Result<[Product], Error> { ... }

func fetchCustomers(products: [Product]) async -> Result<[Customer], Error> { ... }

func fetchOrders(customers: [Customer]) async -> Result<[Order], Error> { ... }

Notice how these calls can be chained without any additional nesting for each call.

1 Like

This is a solution that is close to the ergonomics of the one i proposed myself but might be a bit easier to read. I would like to avoid the try! but I think it is a pragmatic solution in this case.

Result where Failure == Error is exactly the same thing as throws. Nobody else needs to suggest it, because you already did, and adopted it, just with a harder to use spelling.

5 Likes

You don't have to nest the success handling.

let products: [Product]
switch result {
    case .success(let p):
        products = p
    case .failure(let error):
        // handle error
        return
}

// handle success

My example did not include a try!.

Have you looked at Result.map/flatMap? You may need to write overloads that support async, but otherwise it could look something like...

let inventoryResult = await fetchProducts()
  .flatMap { await fetchInventory(for: $0) }
2 Likes

As @JuneBash said, this is what map and flatMap are for. If you throw zip into the mix as well and you can do quite a bit. Good stuff about how to use all three together to make declarative data manipulations over at Pointfree in their collection on those three operations: Map, Zip, Flat‑Map

And with async extensions you can basically turn Result into a promise type.

func then<NewSuccess, NewFailure>(onSuccess: (Success) async -> Result<NewSuccess, NewFailure>,
                                  onFailure: (Failure) async -> Result<NewSuccess, NewFailure>) async
    -> Result<NewSuccess, NewFailure> {
    switch self {
    case let .success(value):
        return await onSuccess(value)
    case let .failure(error):
        return await onFailure(error)
    }
}

The syntax is not great now, but if you want to make async calls one by one, try this:

func doSomething() async {
  let productsResult = await fetchProducts()

  let products: [Product]
  switch productsResult {
    case .success(let success): products = success
    case .failure(let error): return await handleError(error)
  }
  
  let productImagesResult = await fetchProductImages(products)
  // ...
}

There ere some discussions about improving of syntax:

guard-catch with typed throws can make such things easier.
Result can be easily transformed to typed throw, if these features come into play. So may be in future soothing like this can be possible:

extension Result {
  func getValueOrThrow() throws Failure -> Success {
    switch self {
    case .success(let success): return success
    case .failure(let error): throw error
    }
  }
}

func doSomething() async {
  guard let products: [Product] = await fetchProducts().getValueOrThrow() catch {
    return await handleError(error) // the error here is of concrete type, the same as in Result
  }  
  let productImagesResult = await fetchProductImages(products)
  // ...
}
1 Like

Do you need to handle the error from each function separately? If not, then my example is the best solution.

1 Like
let inventoryResult = await fetchProducts()
  .flatMap { await fetchInventory(for: $0) }

I am not sure how this code will look if I also want to handle the two error cases?

func then<NewSuccess, NewFailure>(onSuccess: (Success) async -> Result<NewSuccess, NewFailure>,
                                  onFailure: (Failure) async -> Result<NewSuccess, NewFailure>) async
    -> Result<NewSuccess, NewFailure> {
    switch self {
    case let .success(value):
        return await onSuccess(value)
    case let .failure(error):
        return await onFailure(error)
    }
}

If I use this for multiple sequential calls, and I want to bail out if there is an error, I am back to nesting. Or am I missing something here?

This was just an example. Write your own extensions to give yourself the interface you want.

I do not think it is possible to get the interface I want. I think Dmitriy_Ignatyev sums it up very well in their reply.

I'm pretty sure it is, but you'll have to try and actually build it.

I have played some time in playground, the code now looks like:

func doSomething() {    
  fetchProducts()
    .mapSuccess { products in
      fetchProductImages(products)
    } onFailureDoFinally: { error in
      handleError(error: error)
    }
    .mapSuccess { productImages in
      processProductImages(productImages)
    } onFailureDoFinally: { error in
      handleError(error: error)
    }
}
extension Result {  
  @discardableResult
  func mapSuccess<NewSuccess, NewFailure>(onSuccess: (Success) -> Result<NewSuccess, NewFailure>,
                                          onFailureDoFinally: (Failure) -> Void) -> Result<NewSuccess, NewFailure>? {
    switch self {
    case let .success(value):
      return onSuccess(value)
    case let .failure(error):
      onFailureDoFinally(error)
      return nil
    }
  }
}

extension Optional {
  @discardableResult
  func mapSuccess<Success, Failure, NewSuccess, NewFailure>(onSuccess: (Success) -> Result<NewSuccess, NewFailure>,
                                                            onFailureDoFinally: (Failure) -> Void)
  -> Result<NewSuccess, NewFailure>? where Wrapped == Result<Success, Failure> {
    switch self {
    case let .success(value):
      return onSuccess(value)
    case let .failure(error):
      onFailureDoFinally(error)
      return nil
    case .none:
      return nil
    }
  }
}

So, technically it can be done, but I can hardly say it is as good API:

  • it is easy to get a compiler error, the error explanation is hard to understand in long chains
  • it can (and will, I suppose) be incorrectly used and understood
  • control flow is weird

One more variant:

func doSomething() {    
  guard let products = fetchProducts().getValue(onErrorDo: { handleError(error: $0) }) else { return }
  
  guard let productImages = fetchProductImages(products).getValue(onErrorDo: { handleError(error: $0) }) else { return }
  
  guard let processedImages = processProductImages(productImages).getValue(onErrorDo: { handleError(error: $0) }) else { return }
}
extension Result {
  func getValue(onErrorDo: (Failure) -> Void) -> Success? {
    switch self {
    case .success(let success):
      return success
    case .failure(let error):
      onErrorDo(error)
      return nil
    }
  }
}

I've written synchronous variant for simplicity, async variant can also be done. May be it will help

1 Like

This, adapted from my earlier example, seems simpler:

1 Like

I totally agree that there's something missing here and I suggested a solution to this problem here:

With my suggested change, you would be able to write:

guard let products = fechProducts() catch {
    handleFailure(error)
    return
}

Feel free to provide your usage example in the thread, too.

2 Likes