The proposal does not suggest using Result
where throw
is appropriate. It will still be more idiomatic and more efficient to use throw
and catch
where possible. Result
is a way of storing potential errors; the idea here is not to promote it as a primary way of handling errors.
I donât understand the connection between âthis is an enumâ and âthis will replace try-throw-catchâ.
There are, without question, programmers who will use this instead of try-throw-catch. We know this because some of them have posted in the pitch and review mentioning that they already use their own Result
type instead of try-throw-catch. That we canât actually stop them from doing that is clear from the fact that theyâre doing it now. But for everyone else we can make it clear that we donât think thatâs the right way of working with errors.
If it's not an enum it's obvious that switching over it isn't the preferred way. If the easiest way to decompose a Result
is in a do-try-catch construct, I think it would be better. If it's an enum, then most people are likely to reach for for a switch
first.
I also don't understand the "switch is co-equal to do-try-catch". The main feature of do-try-catch is that you can have more than one try
in the same do
block, and you don't have to produce a result value to use it.
One could say that this is all you need:
do {
try handleValue(result.unwrapped())
} catch {
handleError(error)
}
Whereas if the type is an enum you can also write this:
switch result {
case .value(let value):
handleValue(value)
case .error(let error):
handleError(error)
}
Offering too many ways to use the type could be wrong if you look at it from a certain angle, I guess. So maybe Result
should be a struct that privately uses an enum for storage but doesn't expose the enum directly. I think this was the idea behind the comment.
And I think it's something to think about, but I have a feeling it's going a bit far in hiding the implementation details.
I agree on the changes. In particular, it looks great to me that the Error
type parameters is constrained to conform to Swift.Error
, with self-conformance, and the Result
type has minimum APIs.
I want a guideline when to use the Result
type. Opinions seems divided even in the Core Team as follows.
If I correctly understood, it seems like @Joe_Groff suggests that JSONDecoder.decode(_:from:)
returns a Result
and @John_McCall does not. I think errors from JSONDecoder.decode(_:from:)
are something between simple domain errors and recoverable errors defined in "Error Handling Rationale and Proposal". Those errors are caused by some different conditions. nil
is obviously insufficient to represent those errors. Also they don't require manual propagations. However they are not "systemic errors", and throws/try
does not allow us to specify their error type (without typed throws). Should JSONDecoder.decode(_:from:)
return a Result
?
Of course. But when one has a Result
on one's hands, how does one access its innards? If it's an enum
, I believe most people will reach for a switch
, and I'm not convinced that's the right outcome.
Here's another way to pose the question: is making Result
decomposable in a switch
statement essential to its usefulness? I don't think so.
I mean, without pattern-matching over an enum
that code just looks like this:
if let value = result.value() {
handleValue(value)
} else {
handleError(result.error()!)
}
I don't see how that's semantically discouraging in any way. Literally nothing about making this not an enum
, or making it harder to do pattern-matching over it, would change the extent to which it supplants normal try-throw-catch error handling.
Please also include updating the official Swift documentation with how this new type fits in with the overall error handling approach in Swift too.
I didn't chime in much during the original review period, and I've also been pretty hesitant/skeptical about the value of Result
when compared to Swift error handling and future async capabilities.
With that in mind, I hit a situation very recently (doing some recursive processing with SwiftSyntax's SyntaxVisitor
) where I needed to account for failure cases, and the visitor's methods aren't declared throws
-ing. Instead I would need to store "either the result or an error" as state to retrieve later, and well, Result<T>
is precisely the way to model that.
Situations like this will surely continue to arise where we need to integrate with APIs that make it difficult or impossible to use traditional Swift error handling, or where the value-or-error outcome of a computation needs to be stored in a more long-lived fashion. In those cases people will reach for a type like this, so I've warmed up to the idea of the standard library providing a vetted implementation and best practices around it. I'm very happy with the above changes to the original proposal (especially the Error
constraint, which I feel is absolutely mandatory).
I think the idea is to make such things difficult by not providing value
and error
accessors, only unwrapped
. This is a way to make a statement about the right way to use Result
: you always unwrap and have to handle the throw. I think it's a valid design, but I worry it'd be a bit too strict.
Oh, I see, that does make sense. Yeah, I don't think that's a direction we'd want to take.
Now I know the discussion has been had at least a little bit!
I would prefer a more restrictive shape, as nicely expressed by Michel, but I'll be happy to have a standard Result regardless.
The easiest way to get the value out of a result with this proposal seems to me like it'd be to use the unwrapped()
method rather than pattern matching with switch, which will give you a value you can use locally in expressions or else throw the captured error to be handled by catch
somewhere.
Example about URLSession in revised proposal includes a wrong.
URLSession.shared.dataTask(with: url) {
(result: Result<(URLResponse, Data), Error>) in // Type added for illustration purposes.
...
}
This signature of closure has lost URLResponse
when error happens.
Source:
If the request fails, the
data
parameter isnil
and theerror
parameter contain information about the failure. If a response from the server is received, regardless of whether the request completes successfully or fails, theresponse
parameter contains that information.
I hope you're right. I agree (obviously) that unwrapped()
is clearly the better way, but I doubt it will be preferred in practice; in fact I expect it will be used in a minority of cases. I must be a pessimist.
Could there be a conditional version of map
which takes a throwing transform when Error == Swift.Error
?
I still worry that having a typed Error
(and typed throws in the future) will not be worth the programming complexity, and that additional error handling complexity opens the door for misunderstanding and misuse.
I'm still concerned that this is being proposed without other proposals as a driving factor at the language level (such as annotations on callbacks to generate Swift code that can use Result<T,E>
rather than (T?, E?)
signatures). Doing this now without a language driver seems to arbitrarily give up a chance at having the design be driven by an a language/compiler use case, rather than as a theoretical.
But given the ship has sailed, I'll think this looks like a good proposal for how Result should start.
Named map
, I think that would be somewhat confusing for people. If it weren't named map
, it might be okay, but I think we'd want to wait before accepting it in the standard library just to see whether it's really generally useful. This won't be our last opportunity to add useful APIs to this type.
Ah, I misunderstood the API when I was revising it. The new Result
doesn't allow perfect typing of the result here, but I think the best translation might just be (Result<Data, Error>, URLResponse?)
.
EDIT: I've been informed that my original version was correct; receiving an Error
is exclusive with receiving a URLResponse
.