Typed throws

My main issue in this whole thread is, that we are coming up with ideas, arguing a little bit, speaking about detail A, B or C and then we are stopping at relevant decisions (especially the hard ones). To me it's unclear who decides what? Is it democratic? Is it done by the core team? Should we vote?

Without a decision process it's non deterministic which does not mean we don't find a solution it's just not guaranteed. :thinking:

But maybe there is one and you can help me out. I can summarize solutions, come up with my own solutions. Rework things we proposed, but to me it feels very undirected. And I don't want to spent my time like this, though I enjoy to learn from clever people like in this forum.

Here is my decision tree from the last weeks:

  1. Decide on sum types (auto generated tagged unions A | B | C)
    2a. If sum types are an option, take a deeper look at error type propagation as a whole in Swift
    2b. If sum types are not an option, go on with the proposal on typed throws (using manual error type conversions)

I assumed we are already at 2b, that's why we wrote the proposal like we did. If there is nothing new to this, we should go on with the proposal, which would be:

  1. Decide on rethrow. As far as I can say we did that.
  2. Solve the throw and type inference issue.
    4a Keeping source compat: swift-typed-throws/Typed throws discussion.md at master ¡ minuscorp/swift-typed-throws ¡ GitHub
    4b Not keeping source compat: Typed throws - #123 by torstenlehmann

Do I get you right, that this would mean, that the second parameter is always catched? And ErrorType is a keyword to infer the Error type from the tracked parameter?

Thank you for the amazing summary of issues @xwu!

This whole proposal is a careful balancing act between the benefits of increasing the modeling power of the language (e.g. some low-level efficiency issues) and the cost of increasing the complexity of the base language.

Much discussion has been expended trying to figure out how to handle contexts that are statically determinable to be able to throw T and throw U, e.g. by boxing into an Either type implicitly (assuming that neither T nor U is a subtype of the other).

I'd like to strongly encourage a direction that does /not/ worry about or consider this, particularly with a goal to simplify the implementation and surface area complexity of the feature. A context like the above could/should just immediately type erase to Error. In addition to being simple, general, and predictable, this is important for compatibility with existing logic.

While it is true that we could go further here, I don't see a reason to provide language support for implicit typed propagation in this case. People who want such a thing can write the catch/box/rethrow logic manually (or possibly use a helper that does it). The advantage of eschewing this is that it pushes complexity out of the language and /into user code/. While some fans of typed error propagation will be sad about the ergonomic hit in some cases, I think it is a good middle ground between allowing code with a single class of domain error to be expressible, and also allows folks who care about low level efficiency to get what they want (at the cost of more manual code).

This is a long way of saying that I personally don't think the ergonomic benefit of doing something here is worth the complexity, either in implementation complexity or conceptual complexity for the feature.

I would also recommend punting "typed rethrows" out to a separate discussion. It may not be necessary, and this is already a complicated-enough proposal. It can be added/defined in a future proposal if it warrants its own complexity (something that isn't clear to me).

-Chris

12 Likes

Just to make that clear. That means, if rethrows does not has a type, we need to rethrow Error in all cases. This is because the error of the throwing function parameter can be converted internally to another error (current Swift behaviour) and if we can't express that, we need to assume Error in all cases. See https://github.com/minuscorp/swift-typed-throws/blob/master/Typed%20throws%20discussion.md#solution-1 (rethrow with converting).

And going further. If we need to erase the error type for every higher order function of the standard library then I see no substantial benefit in comparison to Result, comparing catch-Clauses for typed throws with switch for Result.

func testTypedThrows() {
    do {
        let cat = try callCat()
        print(cat)
    } catch let error as CatError {
        // error is CatError
        print(error)
    }
    // exhaustive
}
func testResult() {
    let catResult = callCatResult()
    switch catResult {
    case .success(let cat):
        print(cat)
    case .failure(let error):
        // error is CatError
        print(error)
    }
    // exhaustive
}

Maybe typed throws is a little bit more readable. But using higher order functions it would fail.

func testTypedThrowsArray() {
    do {
        // error type gets already erased in map
        let cats = try (1...3).map { _ in try callCat() }
        print(cats)
    } catch let error as CatError {
        // error is CatError
        print(error)
    }
    // not exhaustive!
}

On the other hand with Result I could do this

func testResultArray() {
    let catsResult = Result.all((1...3).map { _ in callCatResult() })
    switch catsResult {
    case .success(let cats):
        print(cats)
    case .failure(let error):
        // error is CatError
        print(error)
    }
    // exhaustive!
}

where

extension Result {
    static func all(_ results: [Result<Success, Failure>]) -> Result<[Success], Failure> {
        var successValues = [Success]()
        for result in results {
            switch result {
            case .success(let success):
                successValues.append(success)
            case .failure(let error):
                return Result<[Success], Failure>.failure(error)
            }
        }
        return Result<[Success], Failure>.success(successValues)
    }
}

So I just can layout the consequences here. With no typed rethrows the benefit of typed throws gets even smaller. So small that I would further rely on Result in favour of composability and the possibility to come up with my own operators.

1 Like

Yep, I have the same feeling. Either typed-throws-rethrows or just keep current untyped everything. Typed-throws but without typed-rethrows support seems like a half-baked solution.

The idea in this pseudo example was that main function will require you only to write try if the compiler knows that the first closure throws anything other than Never. The second closure parameter would not affect this, hence (.parameter(1)). The current default for rethrows would equal (.all) in my pseudo code.

This would basically give the library author and user more control around the whole rethrows mechanism.

Let's be realistic, it's already hard to implement what we have scheduled. I'm in favor of some kind of error propagation. But this is already a pretty big implementation so in order to make things reasonable, I think we should stick with typed throws and bring rethrows in another proposal.

Just to reiterate, this is an idea, without any priority. Just because I answered a question, doesn't mean I'm trying to push something like this in this thread. ;)

I feel the same way. If every rethrows erased the thrown type to Error then we would cut out every higher order function from the use of this new feature. IMHO we better do all of it, to get as much value out of this feature, or we leave this idea alone. A half-baked solution does not seem to fit Swift.

4 Likes

Without a final decision about how a supposed typed rethrows would fit in the compiler without breaking current source it feels difficult to implement it and make everyone happy. I appeal to the community to think about a solution that I think it won't fit in this proposal but in a "Typed error propagation".
It'll need iterations, as many features of the language but I think what is proposed here is a solid first step about what the intentions of the community are and stablish a path for the native error handling.

Sigh (not directed at you) - yes you're right, that would be really annoying.

Questions/thoughts for you (sorry I missed a chunk of the thread above, and don't have all the context in my head):

  1. Do people actually remap errors in rethrows functions, or is this just a theoretical concern? What cases will that actually fail for?

  2. There are no existing functions that take typed errors, so we can definitely define yourThing(_ fp: () throws E -> ()) rethrows as only being able to rethrow E, automatically. This doesn't help with existing code like map of course.

  3. I don't see how introducing a "typed rethrows" concept helps with this, since this is mostly about legacy code.

Two different thoughts on what can be done here, because #1 is the most important question.


My wild guess is that almost nothing remaps errors. If this is correct, then I would recommend that we make map and everything else in the system do the right thing (for typed errors) and produce a dynamic error if someone remaps the error to the wrong thing.

This will be a hand grenade for the small number of APIs that remap, and there may be cases where that is important. To handle that, introduce a new @i_use_rethrows_but_remap_errors attribute and sprinkle it on the few functions that need it.


If this is unacceptable for compatibility or if remapping is widely used, then you can make rethrows default to erasing to Error, and introduce a @i_dont_remap_errors. This can be adopted by the standard library and many other things using rethrows without ABI or other issues. This is the typical way we'd role out increasingly strict things in the ObjC universe.


Overall, yes, I agree with you we need a good answer for map and other higher order functions.

-Chris

4 Likes

Excuse me if I missed something, but I got a thought that might solve the rethrows problem:

We could generalize the throws-ness of any function to be always throws where essentially non-throwing functions are specifically throws Never much the same way as functions with no return type are actually returning Void:

func one()
// is the exact same as
func two() throws Never -> Void

This would allow writing code that is generic over the error thrown from a function:

func generic<E: Error>(_ function: () throws E->  Void) throws E {
    try function()
}

Notice how the above function perfectly mimics the behavior of what would now be obsolete rethrows. Notice also, that since we explicitly specified the type of the thrown error, we are no longer limited to simple synchronius calls to the function and can get the error from anywhere, including DispatchQueue.sync, which wasn’t possible before.

If at the call site the error E is known to be Never, then the compiler uses the regular non-throwing rules.

1 Like
func generic<E1: Error, E2: Error>
(_ function1: () throws E1 ->  Void,
 _ function2: () throws E2 ->  Void)
 throws (E1|E2|E3)? 
{
    try function1()
    do
    {
      try function2()
    }
    catch //should be E2 here
    {
      if (cond) { throw E3() //E3:Error too}
    }
}

E1 or E2 or E3 which Error type should be thrown out?

1 Like

This thread is getting overly long that people stops reading the earlier comments before replying (not pointing fingers at anyone). A few times already that they rehash the questions about current rethrows semantic, or propose the exact same solution. It doesn’t help either the each rehashing is about the same length.

2 Likes

It’s definitely not the language’s problem. In my opinion, the whole concept of rethrows, that has lead people to believe that this was the language’s problem to solve, was a quick way of mitigating a source of boilerplate caused by a lacking language feature.

In your scenario, if the API author decided that it’s reasonable for a single function to re-throw two different error types, then clearly it’s their problem to come up with a single error representation that can somehow encapsulate Either<First, Second> of them (pun intended).

In other words, your question is essentially the same as asking:

func foo(isTrue: Bool) -> ??? {
    let some: Int = 42
    let other: String = “42”
    if isTrue {
        return some
    } else {
        return other
    }

What should the compiler understand ??? to be?
The obvious answer, in my opinion, is: “If you think it’s reasonable to mix the unmixable, then it’s your problem to figure out how to actually do the mixing”. The most trivial way to do it would be to find the closes common supertype and mindlessly use that (which in the most generic cases is Any, or, in the context of throwing, Error). But again, automatically making API decisions on behalf of the API author is a bad idea, so the compiler should do absolutely nothing to help come up with an error type.

Edit:

Purely for short-term compatibility sake, rethrows should type-erase any non-Never error type to Error and leave Never alone. This way, rethrows gets to retain its exact behavior. If someone doesn’t want to have their error type lost, then they shouldn’t use a deprecated language feature in the first place.

Perhaps we should build up an error throwing manifesto with an FAQ-style summary of all the points that were made in this thread to avoid any further rehashing

I never want to encounter code like this.

An API which restricts the types of errors that closure parameters are allowed to throw, is diametrically opposed to what I want in an error-handling system.

If an unhandled error occurs at runtime, then the program should crash. It should not fail to compile because such an error might occur—the entire point of runtime error-handling is to recognize that such errors indeed might occur at runtime.

It’s one thing to say that an API can be marked as only ever throwing one particular type of error, so that catch clauses can be simple and exhaustive. I’m not convinced it’s valuable enough to warrant a change to the language and the additional complexity it brings, but it’s at least understandable.

However, restricting the permitted error types of an input parameter is purely anti-ergonomic. I do not see any benefit from that.

If I want to call yourThing with a closure that can throw MyError, that should just work. If the error is caught somewhere (like a catch in my own code), then it’s fine. And if it goes uncaught, then the program crashes just as it should.

That is how error handling works today, and should continue to work in the future.

The language should not prevent me from passing in that closure. The only effect of such a restriction would be an increased amount of boilerplate and glue code that must be written by every user of the API.

I am strongly opposed to letting API authors restrict the types of errors that can be thrown by closure parameters.

2 Likes

There are linked in the first post different sources of information about this.

Really :heart_eyes: the idea but I think it should be optional.

Yes and 6 clicks on the discussion document are far too less compared to the amount of people discussing about the topics mentioned in this document. We can further develop it into a FAQ, but can we please (and I'm really bored of repeating this) first come up with potential solutions for the two issues that are mentioned in the discussion document: https://github.com/minuscorp/swift-typed-throws/blob/master/Typed%20throws%20discussion.md.

Otherwise we can also start two new threads where we discuss these two topics separately, but I don't see any progress on this since days. It is just repeating the same stuff again and again, which may have value for the community as a whole, so that the idea spreads wider, but it's not efficient.

Entering this discussion without even reading the proposal or the discussion document is disrespectful to the people that put in a lot of thought and work over the last weeks from my point of view. It's not needed to read the whole thread. We have a summary for you. This is the idea of the discussion document where we link to the posts in the thread. We want that everyone on the forum has the opportunity to participate in the discussion, but reading these two documents first should be mandatory.

1 Like