Typed throws

This is my favourite solution for

  • not breaking source compat
  • don't advancing the throws keyword (e.g. @typed throws)

But maybe the majority prefers advancing the keyword more. But I will point this out in the discussion document. Thx a lot.

We put together all the approaches we have until now here: https://github.com/minuscorp/swift-typed-throws/blob/master/Typed%20throws%20discussion.md#throw-and-type-inference

1 Like

We are now three people working on the implementation, but we all have little to no experience with the Swift compiler whatsoever. This gets tricky, especially when we try to implement type-checking (the swift type-checker is the single most scary piece of code I've ever seen). I spent the whole afternoon, only trying to implement type-checking of the type after throws.

Would someone with a bit more knowledge about the type-checker volunteer, to at least give us some tips? That would already really help. Just DM me ;)

I would recommend posting your implementation related questions on #development:compiler. We can continue talking about type-checking the throws type there.

Just for let you know, we're still in development, it is a pretty big proposal to implement :smile:

8 Likes

Let me show an option though I am not sure if it is good.

Currently, it is discussed that the following two are equivalent.

func foo() throws
func foo() throws Error

In the context of "Improving the UI of generics", it can be written as shown below.

func foo() throws any Error

Instead of it, I think plain throws can be interpreted as throws some Error.

func foo() throws some Error

throws some Error has an advantage of performance because it does not require existential containers when it is specialized.

A problem of throws some Error is cases of throwing multiple types of errors in one function.

// `throws` stands for `throws some Error`
func foo() throws {
    if Bool.random() {
        throw AError()
    } else {
        throw BError()
    }
}

On the analogy of opaque result types, it seems like to cause a compilation error. However, because Error has self-conformance, any Errors can be throws as some Errors. Thus the code above can be interpreted as the following one.

func foo() throws some Error {
    if Bool.random() {
        throw AError() as any Error
    } else {
        throw BError() as any Error
    }
}

Then it could be possible to introduce throws as throws some Error without any source-breaking changes.

3 Likes

Just wanted to say thank you very much, @minuscorp, for pushing for this idea.

1 Like

I've been out of the forum and the implementation due to several health issues, but this is not an idea I want to abandon at all. Once I'm recovered I'll try to start again and get this forward. Thank you for your comprehension and support, @eneko and the rest of the supporters.

1 Like

I wish you a speedy recovery. It might interest you that typed throws came up in another thread related to rethrows and protocol conformances. There's a design challenge buried in there from me ;)

Doug

In the first post there are documents that redirect to the pitch in the thread as well as some workarounds for some edge cases that came up, just in case you didn't notice them.
Anyway I'll read throughly the thread you mention and see if something we talked about in here can help you out. Also we left a pending implementation that might be useful in the future for the first step, that was make typed throws work into the system. Later iterations would solve subsequential issues until a final solution is ready to be proposed, that was our point of view about this whole topic if this post helps in something to you.

I’m a bit late here but

At most one type of specific error can be used with a throws .

Is this rule set in stone?

I understand that “sum types” are out of scope but, in my opinion, listing types is not a “sum type” but just, well, a list of types.

I would very much like to have a list of types thrown.

I think it would simplify many use cases, e.g. the family example would not have to be:

enum FamilyError : Error {
   case kid(_ e: KidError)
   case spouse(_ e: SpouseError)
   case cat(_ e: CatError)
}

func callFamily() throws FamilyError -> Family {
    do {
        let kids = try callKids()
        let spouse = try callSpouse()
        let cat = try callCat()
        return Family(kids: kids, spouse: spouse, cat: cat)
    } catch let error as CatError  {
        throw FamilyError.cat(error)
    } catch let error as SpouseError  {
        throw FamilyError.spouse(error)
    } catch let error as KidError{
        throw FamilyError.kid(error)
    }
}

but could be much simpler:

func callFamily() throws CatError, SpouseError, KidError -> Family {
    let kids = try callKids()
    let spouse = try callSpouse()
    let cat = try callCat()
    return Family(kids: kids, spouse: spouse, cat: cat)
}

I understand this just moves the catches to the call side. On the other hand, switching over FamilyError is the same amount of code.

For Result and rethrows, the compiler could use the “natural sum type”: Search the inheritance tree until a type is found that all thrown types conform to (the search is guaranteed to end at Error):

protocol ColoredError: Error { }
class BlueError: ColoredError { }
class DeepBlueError: BlueError { }
class RedError: ColoredError { }

func f() throws BlueError, DeepBlueError, RedError {}

is equal to

func f() -> Result<Void, ColoredError> {}

How are you supposed to catch this type of typed error? The point of of throwing just one type is the same rationale that a variable can only have one concrete type, to achieve what you propose I think you'll have to cast all types individually to check which one is, and if they are enums then cast which case is. Two levels of casting seems to complex to me. I don't know about other opinions though!

Imho it would already help if that annotation wouldn't change the behavior of the compiler at all (except maybe checking wether the error-types actually exist):
Documenting which errors users of your code can expect is useful on its own, even if there are no guarantees associated with that information.

It can be a thing to expand in a future work or a second iteration, first of all to let the user get used to the new syntax (unique thrown type) but throwing for example, a list of unrelated types, gives almost as little information in the client error treatment as if it were an untyped thrown error imo

As you do today:

} catch (let e as CatError) {
} catch (let e as SpouseError) {
} catch (let e as KidError) {

or, for the other example, you might as well use:

} catch (let e as RedError) {
// First match, most specific.
} catch (let e as ColoredError) {
// Catch everything else
}

I think it would be fine if you had to catch the “natural sum type” unless you catch all types declared in the throws-list.

With this approach we do not was ease the client part of a typed throws handling, it seems to me like just the same approach as we have today, but somehow those are typed when in practice they are the same as if they weren't.

Also, in the proposal all the subclassing topics are recorded and documented, maybe we missed something, but I think we do not need several typed errors, as it makes the handling and the writing linearly complex, as if you use subclassing with a single typed error that problem is gone.

As of Swift 5.3 thanks to SE-0276 we can now also handle typed errors as

} catch ColorError.red, ColorError.blue {
  // handle these cases
}
1 Like

They are untyped though (the interface)

That should be corrected in the draft then, as you could handle same type errors in the same catch clause.

I really wish this topic would move forward again. This, together with all the async work being done currently would have been awesome to work with. :+1:

6 Likes
Terms of Service

Privacy Policy

Cookie Policy