Status check: Typed throws

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

Yeah, I'm also a bit sceptical that this is a fruitful direction, when taken to that extreme at least. In contrast, it'd be relatively trivial to have it function merely best-effort within whatever scope the build system is naturally operating at (whether per-file, whole-module, or whole-program for e.g. LTO). I think that simpler version is an easier sell (but it's really a tooling question, so you'll need to convince the Xcode team (or similar) that it's of high value).

1 Like

I was concerned that would be the case, but was hopeful nonetheless. Doesn’t hurt to ask the question. Thank you for the erudite responses.

Many people are saying that API stability would be a problem, I think.

Let's say I have the following function in my library:

func getContents(ofURL urlString: String) async throws (NetworkError) -> Data {
    let url = URL(fromString: urlString)!
    return try await getData(url)
}

But now I decide that I want to throw a URLParsingError when the URL parsing fails. Since I already committed to the fact that getContents(ofURL:) only throws a NetworkError, I cannot just throw another error, so I have to change my function signature, which could be a source break for many clients of my library. Therefore I can only make this change in a new major version of my library. This dilemma could have been avoided, had I just used an untyped throws instead.


Although I can understand this argument against typed throws, I personally don't find it that compelling. Sure, some people can and will misuse typed throws, but most people won't, since we will leave untyped throws as the default. I can also see that it is actually nice, that I could actually notify the users of my library that there is a new error that might be thrown from a function which they might care about. With untyped errors, users often write catch-all clauses that aren't that helpful, because they either just crash or just print the unknown error.
Also, I think it's a bit odd that the thrown error is singled out as something that should not break sources when changed, while everything else about a function (argument types, return type, etc.) still does. What is the big difference?

Yeah, but it's already true - maybe later you decide it should return a String instead of Data, or a file handle, or whatever else. Or you decide it should really take an actual URL rather than a String.

This does raise a question, however, about overloading. I think we've all been assuming you can't overload a method based on its throw type(s). Although my instinct is to concur with that presumption, it's worth exploring.

If throws overloading isn't permitted, then that does become a meaningful distinction, as function parameters and return types can be overloaded in Swift (although the latter is apparently widely regarded as a mistake, due to its burden on type inference). Overloading is not a panacea but sometimes is a useful escape hatch for what would otherwise have to be an ABI-breaking change.

Yeah, that's an aspect of the counter-position that I'm still puzzled by. Is it that opponents are concerned as some kind of matter of principle - that they think nobody should have the functionality even if it doesn't effect them - or is it a concern that in practice there'll be some "viral" aspect which will coerce them into using typed throws when they don't want to? If the latter, I'd like to see that demonstrated a bit more clearly, as I don't see any reason why untyped catch clauses - i.e. } catch {, as are very common today - wouldn't continue to be universally acceptable (to the compiler) and effectively defend borders.

3 Likes

Hey all,

A quick update: I've kicked off a new pitch thread with a complete proposal.

Doug

11 Likes

Should we allow a comma list of error types rather than limit it to one type of error?

I’m going to close this thread now, further discussion should happen on the new pitch thread.

2 Likes