[swift-evolution] [Pitch] Typed throws
Matthew Johnson
matthew at anandabits.com
Tue Feb 28 11:19:44 CST 2017
> On Feb 28, 2017, at 11:08 AM, Vladimir.S <svabox at gmail.com> wrote:
>
> On 28.02.2017 19:48, Matthew Johnson wrote:
>>
>>> On Feb 28, 2017, at 6:47 AM, Vladimir.S <svabox at gmail.com> wrote:
>>>
>>> On 28.02.2017 0:40, Matthew Johnson via swift-evolution wrote:
>>>>
>>>>> On Feb 27, 2017, at 1:46 PM, David Waite via swift-evolution
>>>>> <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
>>>>>
>>>>> IMHO, there are two kinds of responses to errors - a specific response,
>>>>> and a general one. Only the calling code knows how it will deal with
>>>>> errors, so a “typed throws” is the function guessing possible calling
>>>>> code behavior.
>>>>>
>>>>> The problem is, that gives four possible combinations - two where the
>>>>> function guesses correctly, and two where it doesn’t. The most damaging
>>>>> is when it suspects a caller doesn’t care about the error, when the
>>>>> caller actually does. This is unwanted wrapping.
>>>>>
>>>>> To provide an example, imagine a library that parses JSON. It has several
>>>>> errors indicating JSON syntactic errors, and an “other” for representing
>>>>> errors on the input stream. It wraps the input stream errors so that it
>>>>> can provide a closed set of errors to the caller.
>>>>>
>>>>> The caller is responsible for returning a data set. It doesn’t think that
>>>>> code calling ‘it” cares about JSON syntactic errors, merely that the
>>>>> object was not able to be restored. It returns its own wrapped error.
>>>>>
>>>>> However, the original caller knows it is loading from disk. If the
>>>>> problem is due to an issue such as access permissions, It has to know
>>>>> implementation details of the API it called if it wishes to dive through
>>>>> the wrapped errors to find out if the problem was filesystem related.
>>>>>
>>>>> Add more layers, and it can be very mysterious why a call failed. Java at
>>>>> least captures stack traces in this case to aid in technical support in
>>>>> diagnosing the error.
>>>>>
>>>>> Wrapping exceptions also prevents an aggregate of errors from different
>>>>> subsystems being handled as a category, such as having a catch block
>>>>> handle RecoverableError generically
>>>>
>>>> Fwiw, I think wrapping errors is something that people are sometimes going
>>>> to want to do regardless of whether they are typed or not. Maybe the
>>>> solution is to better support wrapping errors by focusing on the problems
>>>> that wrapping causes. For example, we could do something like this to make
>>>
>>> Just to clarify, do you think about something like this? :
>>> (pseudocode, sorry for mistakes)
>>
>> Is the question about the ability to create `fullErrorStackDescription`? Yes, that would certainly be possible to implement in an extension.
>>
>> I’m not sure why your examples use `rethrow` to rethrow the error instead of `throw`. Was that a mistake?
>>
>> I also want to reiterate that deciding when and how to wrap errors is something that requires careful thought and judgment. The goal is to provide a stable and ergonomic way for callers to learn about and handle errors they are likely to care about. There will often be an “everything else” case and that’s ok.
>>
>> As others have pointed out, the important thing is that it is easy to identify cases where I can *improve* UX by recovery or specific messaging to the user. The fact that there will usually be a default path is a given, but it’s best to avoid that path if possible.
>>
>
> Well, I was trying to figure out how the some code could looks like with typed throws, taking into account your suggestion about built-in underlyingError and originalError props in Error protocol and the idea of "Maybe the solution is to better support wrapping errors by focusing on the problems that wrapping causes".
> As for 'rethrow' it is not a mistake, but hypothetical use of the keyword to rethrow underlying error inside own error without boilerplate code, i.e. "better support wrapping", compare:
>
> catch let e {
> // need to somehow inject the current e instance into our error instance
> // (or with some other syntax)
> throw BarError.fooRelatedError(SomeType1(), underlying: e)
> }
>
> and
>
> catch {
> // 'rethrow' can clearly say that the current error instance will be injected
> // into our own error instance in underlyingError prop. we focuse only
> // on our own error instance
> rethrow fooRelatedError(SomeType1())
> }
>
> Sorry for not clarifying all this in first message.
Ahh, ok. I think the best way to accomplish something like this would be with some kind of general error propagation, as has already been discussed. With a mechanism like that you wouldn’t have to catch the error at all when you’re just wrapping it.
>
>>>
>>> func foo() throws {}
>>>
>>> func bar() throws {}
>>>
>>> enum BazError: Error { case baz1, case baz2 }
>>> func baz() throws(BazError) {..}
>>>
>>> enum BatError: Error {
>>> case fooRelatedError(SomeType1)
>>> case barOrBazRelatedError
>>> case specialErrorOne(Int)
>>> case specialErrorTwo(String)
>>> }
>>>
>>> func bat() throws(BatError) {
>>> do {
>>> try foo()
>>> }
>>> catch {
>>> // underlyingError will be injected
>>> rethrow .fooRelatedError(SomeType1())
>>> }
>>>
>>> do {
>>> try bar()
>>> try baz()
>>> }
>>> catch {
>>> // underlyingError will be injected
>>> rethrow .barOrBazRelatedError
>>> }
>>>
>>> ..
>>> if flag1 { throw .specialErrorOne(intValue) }
>>> ..
>>> if flag2 { throw .specialErrorTwo(stringValue) }
>>> ..
>>> }
>>>
>>> and then
>>>
>>> func test() {
>>> do {
>>> ...
>>> try bat()
>>> ...
>>> }
>>> catch let e as BatError {
>>> switch e {
>>> case fooRelatedError(let some) : { print(some, e.underlyingError) }
>>> case barOrBazRelatedError : {
>>> print("something with bar or baz, so try this:..")
>>>
>>> if let bazError = e.underlyingError as BazError {
>>> switch bazError {.....}
>>> } else {
>>> // do something about "bar" error
>>> }
>>> }
>>> case specialErrorOne(let i) : { print(i) }
>>> case specialErrorTwo(let s) : { print(s) }
>>> }
>>>
>>> log(e.fullErrorStackDescription) // BatError.description + underlyingError.description + underlyingError.underlyingError.description etc
>>> }...
>>> }
>>>
>>> ?
>>>
>>>> it easier to get at the original error:
>>>>
>>>> protocol Error {
>>>> // The error directly underlying this error.
>>>> // Ideally the compiler would synthesize an implementation for enums
>>>> conforming to `Error`
>>>> // If `self` is a case that has an associate value which is or conforms
>>>> to `Error` that error would be returned, otherwise `nil` would be returned.
>>>> var underlyingError: Error? { get }
>>>>
>>>> // The original error underlying *all* layers of wrapping.
>>>> // If underlyingError is non-nil this is also non-nil.
>>>> var originalError: Error { get }
>>>> }
>>>> extension Error {
>>>> var underlyingError: Error? {
>>>> return nil
>>>> }
>>>> var originalError: Error {
>>>> return underlyingError?.originalError ?? underlyingError ?? self
>>>> }
>>>> }
>>>>
>>>> We could even provide syntactic sugar for catch sites that want to deal
>>>> with the original error rather than the wrapped error if that is an
>>>> important use case.
>>>>
>>>>>
>>>>> An interesting solution that has emerged in Ruby to keep library authors
>>>>> from wrapping exceptions is by decorating the existing exception.
>>>>> Exceptions are caught via pattern matching (same as in Swift), so rather
>>>>> than wrap an extension, they extend the error instance with a
>>>>> library-specific module (e.g. swift protocol). So while the error may be
>>>>> a IOError in ruby, you can still catch it via ‘rescue JSONError’
>>>>>
>>>>> Trying to specify the exact errors becomes even more destructive with
>>>>> protocols and closures, where the person defining the interface knows
>>>>> neither which errors the implementor of the call will throw, nor
>>>>> necessarily if the caller will want to implement specific behavior on
>>>>> those errors. This in my personal Java coding experience almost always
>>>>> leads to wrapping in some protocol-specific Exception type which exposes
>>>>> minimal information to the caller, or exposing your errors in some
>>>>> unrelated type like IOException which was declared based on the author’s
>>>>> experience of possible exceptions.
>>>>>
>>>>> -DW
>>>>>
>>>>>> On Feb 27, 2017, at 5:19 AM, Daniel Leping via swift-evolution
>>>>>> <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
>>>>>>
>>>>>>
>>>>>> On Mon, 27 Feb 2017 at 8:44 Dave Abrahams via swift-evolution
>>>>>> <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
>>>>>>
>>>>>>
>>>>>> on Fri Feb 17 2017, Joe Groff <swift-evolution at swift.org
>>>>>> <mailto:swift-evolution at swift.org>> wrote:
>>>>>>
>>>>>> > Experience in other languages like Rust and Haskell that use
>>>>>> > Result-based error propagation suggests that a single error type is
>>>>>> > adequate, and beneficial in many ways.
>>>>>>
>>>>>>
>>>>>>
>>>>>> And experience in still others, like C++ and Java, suggests that
>>>>>> using static types to restrict the kind of information a function can
>>>>>> give you when an error occurs may actually be harmful.
>>>>>>
>>>>>> +1 here. It becomes wrapping over wrapping over wrapping. Try doing a
>>>>>> big app in Java (i.e. some kind of layered server) and you'll understand
>>>>>> everything. Ones who tried and still want it - well, there are different
>>>>>> tastes out there.
>>>>>>
>>>>>>
>>>>>>
>>>>>> --
>>>>>> -Dave
>>>>>>
>>>>>> _______________________________________________
>>>>>> swift-evolution mailing list
>>>>>> swift-evolution at swift.org <mailto:swift-evolution at swift.org>
>>>>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>>>>>
>>>>>> _______________________________________________
>>>>>> swift-evolution mailing list
>>>>>> swift-evolution at swift.org <mailto:swift-evolution at swift.org>
>>>>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>>>>
>>>>> _______________________________________________
>>>>> swift-evolution mailing list
>>>>> swift-evolution at swift.org <mailto:swift-evolution at swift.org>
>>>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>>>
>>>>
>>>>
>>>> _______________________________________________
>>>> swift-evolution mailing list
>>>> swift-evolution at swift.org
>>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>>>
>>
>>
More information about the swift-evolution
mailing list