[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