Status check: Typed throws

Okay, but what would be the point? You're essentially reinventing opaque result types. Granted, this version would allow returning more than one concrete type, but it would be paid for with a change to the ABI to enable emplacing values on the caller's stack (recursively, because that value may also be returned further up the stack frame). This would only be useful for achieving dynamic polymorphism in an embedded environment. It's my subjective opinion of course, but it feels like embedded environments are unlikely to benefit from dynamic polymorphism, where static polymorphism can do the trick in most of the cases. Especially if that dynamic polymorphism comes at such a big implementation complexity cost.

Well, first off, teaching a compiler to do that kind of locally-unbalanced stack manipulation is a lot more difficult that you think, and it interferes with a lot of other things that want to manipulate the stack. Second, we'd have to do a lot of work in the common case when we're just propagating an error through a frame, which is usually a bad trade-off. Most importantly, though, if any Error is a legal type, there's no reason it's constrained to having exactly one value at once, or that that value can only be in the process of getting thrown; you do have to have rules to support things like storing an any Error into a struct property.

5 Likes

I feel like it'd be simpler to allow enums to take parameter packs, and then provide a way to spell `case {n}(repeat each T), where n is a monotonicly increasing number starting from zero.

Then we could have:

enum AnyOf<each Case> {
    // afformentioned new spelling for repeat case `0...`(each Case)
}

func decompose<T, U>(_ union: AnyOf<T, U>, onT: (T) -> Void, onU: (U) -> Void) {
    switch union {
    case .0(let t): onT(t)
    case .1(let u): onU(u)
    }
}

A | B could even become a shorthand for an equivilant enum defined in the standard library. In this world, it doesn't matter if U == T because you can still tell the difference when matching on it. And it doesn't actually require any new concepts in the language, just the ability to use existing ones in new places.

Of course, it might be better to instead just let A | B be a structural type (like a tuple) equivilant to the above AnyOf, and then we could optionaly name the cases. Then in a varadic context it'd just be (Never | repeat each T)

6 Likes

That sounds very promising! There was a discussion about turning a tuple into a nominal type too, in light of parameter packs. I'm not sure where the discussion went, though. I remember the biggest concern was the labels, if I'm not mistaken. Such a union type may also benefit from a syntax that allows labels. That would put it more on par with tuples. Maybe even ease the process of bridging C unions (I know C unions don't have a discriminator...).

Should the theoretical Union type discussion be spun off to its own thread, since it’s pretty clear this feature will be pitched with a single-type requirement and that’s therefore off-topic here?

3 Likes

Great idea: Anonymous Union Types.

While I don't think this is a good solution for Swift code (the compiler should be able to verify the error type and allow evolution through existing patterns, perhaps with the addition of @frozen enums without module stability) but I think this may be a good solution for Obj-C bridging (or any error bridging that doesn't take part in Swift's checking) to allow for essentially unchecked errors while providing an API which would allow for a single thrown type. I suppose Swift could also provide an annotation like @frozen which would enable the @default catch with module stability (or in general). But I don't think it makes sense as a default behavior.

(I’m going to reply over here since the discussion from the spun-off topic came back around to typed throws)

I think “too lazy to actually think things through” is an unfair characterization. The way I see Swift exceptions (and I am, again, no expert in CS) is that when you are performing operations that could fail in various ways those operations should throw an error, some of which may be recoverable and others won’t be.

What I, as the programmer, should do is reason about what makes sense for me to handle and look for that, and propagate the rest “up the chain” (if there is an “up”) for anything else that can handle it (or bail out completely, if that’s more appropriate). In that way, only the errors that I can do something about matter. The current state of Swift’s Error works well in this scenario as I can catch whatever I know I can handle and either throw or crash for the rest.

I’ve been participating in discussions here for several years now and the topic of typed throws has cropped up several times, and it genuinely excites a handful of community members each time, but I have never seen anyone actually enumerate the benefits of having a throw with a concrete type before this thread. Even then, the only specific benefit I’ve seen clearly communicated here is that it’s necessary for Embedded Swift.

3 Likes

I think there's been more than that. Do you see the parallels between strongly-typed errors and strong typing more broadly? e.g. why can I be specific about the error condition(s) if I return a Result but not if I throw? Throwing clearly has benefits due to its special support in the language (e.g. error handling can be omitted in intermediary functions), why artificially cripple it?

I would like to see the converse position better elucidated. Swift has chosen to be strongly-typed - why does it treat this one case differently? We're allowed to do better than have every function parameter be Any, every (non-error) result be Any, why can't the error results be more specific than Any?

(Error is essentially equivalent to Any - it's a marker protocol, not a functional one)

5 Likes

The proposal from 2020 captures a bunch of the motivation, and will form the basis for a new proposal that we're working on it. It's unfortunate that the initial message in this forum thread didn't link to it.

Doug

4 Likes

You've characterized my point very precisely! In short, people can and do use Result instead of throwing an error and there's a good reason for it. We're merely trying to remove the necessary to reach for Result and give up on the ergonomics of error throwing just to avoid losing static type information.

4 Likes

Kudos for the linked pitch that contains one important point that imo does not receive the attention it deserves in discussion: Code is less self documenting
Knowing that something can go wrong without knowing what exactly might happen is a bad situation.

By coincidence, I just had an unpleasant error handling experience while updating an iPad:
The process started, but then simply failed with a meaningless message ("try again later", "unknown error" or something similar). This is quite frustrating, because obviously, there is a well known reason for the problem, it is just not communicated to the user.

I'm not sure if it is because of the current status in Swift, but If a startUpdate-method had a link to the NotEnoughSpaceError and NotEnoughBatteryError, I would expect that any conscientious programmer calling that function would make sure to handle the errors properly — or, even better, do some checks beforehand, and telling the enduser right away what has to be done so that the update runs smoothly.

2 Likes

I remember reading this at the time. (Wow, 2020 feels like so long ago.) We have had a lot of changes since then. It seems that a lot of the motivation is self-documentation so it’s clear what kinds of errors could be experienced from a given code path. I do wonder if we now have the “technology” to provide another option (or an additional option) that could help the self-documentation goal for everyone, not just those who wish to explicitly enumerate their API’s fail states. It seems like now with swift-format we have a way to scrape a code path to gather information from every possible throw to coalesce error types. Something that does that and generates DocC comments about every possible fail state seems feasible now. (Of course, I’m no expert on this and perhaps this idea is too pie-in-the-sky to actually happen, but I wanted to toss out the idea.)

Having automated generation of comprehensive fail state documentation may meet the use case for the majority of folks who wish for this feature (and doesn’t force anyone into adoption, either).

Is that something that would be possible and/or helpful?

I don't know that it eliminates the need for better type information in the language itself (there's still opaque libraries, resilient APIs, etc), but it would certainly be helpful in any case. Knowing what errors can be thrown in a specific iteration of the code base can be informative to folks working in that codebase, even if it's more happenstance than contract.

That’s why I did try to position that as something “extra” rather than a replacement for the concept.

In all these discussions there’s always been vague warnings about how having typed throws could be problematic for programmers who didn’t use them with intention and careful planning, but I’m not sure I’ve seen anyone lay out how that could be problematic.

In a world where Swift has typed throws, what specifically are the pitfalls an uninitiated developer might fall prey to in their API design?

what if the function being analyzed contains a call to a throwing function from a different module (or different package)?

:thinking:
Perhaps there’d be a way to copy the documented error possibilities from the dependency’s own prior analysis and document that the fail states are not comprehensive when it depends on unanalyzed code?

would the dependency be responsible for distributing its own throws analysis as part of its source code?

I would imagine that binary libraries and closed-source projects would need to opt in, yes, for this to be comprehensive.

no, what i meant is that even for open source repositories, the package maintainer would need to take on the responsibility of checking in these analysis artifacts into the repository, and ensuring that they don’t become stale.

experience with similar responsibilities, such as checking in Package.resolved, does not make me optimistic about the workability of this idea.

2 Likes