MarSe32m
(Sebastian Toivonen)
April 27, 2022, 10:36am
6
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
gahms
(Nicolai Henriksen)
April 27, 2022, 12:45pm
8
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.
Jessy
(Jessy)
April 27, 2022, 6:43pm
9
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
mayoff
(Rob Mayoff)
April 27, 2022, 7:48pm
10
gahms:
switch result {
case .success(let products):
// Handle success
case .failure(let error):
// Handle error
}
but now we are back at nesting. If we want to call another api inside the success we are nested one level more. For each call, the nesting increases.
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!
.
JuneBash
(June Bash)
April 27, 2022, 8:12pm
12
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
afarnham
(Aaron Farnham)
April 27, 2022, 8:15pm
13
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
Jon_Shier
(Jon Shier)
April 27, 2022, 8:25pm
14
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:
It would be convenient to have do-try-catch block in "early-exit style". Like:
do-guard let encrypted = try cypher.encrypt(bytes) catch let e {
print("Encryption error:", e)
return // Thank Adrian for reminding
}
// Massive code
send(encrypted)
I may be wrong, but it's not implemented yet.
Introduction
When error handling was added to Swift in 2015, it intentionally did not allow the expression of precise error types: either a function cannot throw, or it can throw any Error. The error-handling rationale I wrote, which uses an awful lot of words to describe most aspects of the design, gives this one pretty short shrift:
There are many kinds of error. It's important to be able to recognize and respond to specific error causes programmatically. Swift should support easy pattern-m…
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
gahms
(Nicolai Henriksen)
May 9, 2022, 4:09pm
17
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?
gahms
(Nicolai Henriksen)
May 9, 2022, 4:11pm
18
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.
gahms
(Nicolai Henriksen)
May 9, 2022, 4:15pm
20
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
Jeehut
(Cihat Gündüz)
October 8, 2022, 11:36am
25
I totally agree that there's something missing here and I suggested a solution to this problem here:
Motivation
A few weeks ago, I had asked on Twitter if developers prefer error handling in Swift by using throws vs. returning Result. Of the 102 votes, 45% voted for "I prefer Result", which is 40% more than the 32% who voted for "I prefer throws" (source ).
While this small survey isn't representative by any means, it still proves that there's clearly high interest in using Result for error handling (and not only for closure return types), even if that means we have to build nested error types…
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