IMO there are tons of APIs where errors should be partly enumerated to be useful. Like fopen may fail for two dozen reasons. Some of them nicely fit the interface (i.e. errors from incorrect arguments, or access violation), and some of them are pretty much implementation detail (like errors specific for only one underlying FS). So the author can find the right balance between providing interface accuracy and preventing leak of implementations details.
@Douglas_Gregor do you expect to solve the rethrowing protocols problem and spelling something like some throwing AsyncSequence with this effort as well or do you see it as two orthogonal features?
I think in those cases, where some errors are particularly common, some combination of documentation and linting or possibly some "compiler-level lint" feature would be most appropriate.
Going off of the logic in my earlier post, access violation errors in particular I believe would count as a general-purpose error, since you may want to handle those arbitrarily up the call stack without affecting your business logic. For truly domain-specific errors that you want callers to handle as a special case as close to the call site as possible, you'd use a specialized enum type, possibly with a catch-all .other case.
Actually, this is an important and, frankly, far more common use case than any low level optimization. While such an error may come from various different sources it's often critically important to include as much detail as possible to allow for flexible behavior in the consuming layers. Bubbling up error messages from the backend to user, for example, would be far more ergonomic with a typed error. I typically have something like:
enum APIError {
// Should be URLError but URLSession has a bad habit of producing other errors like POSIX Error.
case network(Error)
case response(ResponseError) // Response validation errors.
case backend(BackendError) // Particular types of errors with parsed backend data.
case decoding(DecodingError) // Allow debugging or logging of parsing issues.
case custom(CustomError) // Perhaps there are other validations or higher level issues.
}
This can then be accumulated into higher level errors, like an SDK, which allows fully detailed errors as part of the API surface. As long as Swift allows for automatic erasure to Error, such as trying such an API in normal throws context, it doesn't seem like there should be a constraint one way or the other here.
If there's truly only one way it can fail, some would argue that it should typically just return an optional value. That's certainly how the Stdlib and Foundation are biased.
Things that can fail in only one specific way benefit relatively little from typed throws. Not no benefit - a thrown error can convey more details about the error (e.g. which character the function first barfed on) and you at least still have improved documentation (essentially), but the handling code nonetheless devolves to "either this works or it doesn't, and there's nothing about the failure case that needs conditionalising".
It sounds like this particular "for Embedded Swift" angle is really just wanting a way to return a boolean status code, or a Result. I think @John_McCall touched on this previously - that if there were a convenient way to pass Result quickly through intermediary functions (as there are for native Swift errors with try et al), then that would suffice in place of typed throws [for the "embedded" case].
This is the same argument made by opponents of typed languages in general, re. the assertion that it limits flexibility, increases coupling, increases maintenance burden, etc.
The important lesson from the typed vs untyped language debate is that it's not really about the typing, it's about type inference. Nobody would like Swift if it didn't have good type inference.
So it's likely the same with extending the type system to error conditions. Suitable "syntactic sugar" (type inference or equivalent) will be important to minimise the downsides, in order to let the upsides shine.
Nobody's proposing that you should "need" to as some matter of principle or dogma. I can't imagine the default ever not being throws(any Error) (except in "embedded" mode, of course, where existentials aren't available, or similar).
But you might want to.
Note also that your concerns apply to the type system in general just as much as to errors specifically. e.g. having to copy-paste the arguments for an interstitial function is laborious and increases maintenance burden too. Languages like Python have ways around this (kwargs etc), but Swift doesn't [generally - variadic generics help a bit].
It's very common in [my] code that uses URLSession (or similar) to have a lot of conditional error handling. There's a big difference between .cancelled and .timedOut, for example, let-alone all the protocol-specific error cases like missing authentication, certificate errors, network availability errors, etc. The error handling naturally ends up spread out over various layers because some errors need to propagate further up the callstack than others (in order to find code or local state that provides necessary context for handling them).
Which does suggest a potential use for even more advanced set operations on error types, i.e. subtraction not just union. "I throw URLError except I internally handle cases X, Y, Z, so you just have to worry about the other ones". That's really far into potential future directions, though. At that point it very likely would make more sense to just have full set-theoretic types throughout the language (iff that ever makes sense at all for Swift).
I see (1) being able to solve the rethrows problem for generics and (2) allowing low-level programmers to avoid allocation on error paths as the two primary goals of this feature. Everything else is at best nice-to-have, and we have to acknowledge that the mere existence of the feature is going to be a huge distraction for a lot of people.
A big problem with this "error duality" approach, of having a set of special error cases that can basically always arise from anywhere, is that it makes control flow indeterminate. It's a huge pain to deal with this pattern in Java, as a well-known example.
Just like it's useful to have functions that cannot throw at all, it's useful to have functions that can only throw specific errors. That way callers know what they're getting and can reason about their runtime behaviour more accurately.
This is not hypothetical, either, because we essentially have this today: fatalError. It's very frustrating to be enjoying a 3rd party library that neatly addresses a non-trivial need, only to find that it likes to crash randomly because the author felt fatalError was more "ergonomic" or "flexible" than structured error handling.
I think the reasons Java's exceptions are so painful are that (1) checked exceptions don't interact well with generics and common APIs, (2) unchecked exceptions can be thrown from literally anywhere, easily causing broken invariants, and (3) many types that ought to be "general-purpose errors", like IOException, are checked, so when you want to propagate them arbitrarily up the call stack like you commonly would, you have to change the declarations of every method in between. In Swift, errors are much less precarious because all possible error sites are marked with try and catch to prevent broken invariants, and in the vast majority of cases errors are untyped/unchecked, unlike in Java.
I think the "throwing specific general-purpose errors" approach is misguided because for complex applications, it's precisely because general-purpose errors can be thrown arbitrarily up the call stack (and through arbitrary parts of the codebase) that the number of possible errors quickly becomes impractical to handle exhaustively. For specialized parts of your codebase that all use the same set of domain-specific errors, the syntax for handling them wouldn't have to be any more cumbersome; it'd only be when you let arbitrary general-purpose errors into the mix that you'd have to give up on exhaustiveness checking. In my opinion, the duality adequately handles both cases.
Plus, if you conform domain-specific errors to Error, you can easily create error types where callers can either handle them exhaustively or propagate them as general-purpose errors if they wish; also unlike Java.
Do you mean just in the sense of filling these forums with long threads, or to users of Swift? If the latter, can you elaborate? I feel that in principle typed throws should not add practical complexity to most people's use of Swift; that it should be essentially "opt-in" per Swift's goal of progressive disclosure. I don't see anything in the proposals so far which violate that, but if there is then that should really be highlighted.
A lot of people are going to think, “Hey, I can write my error type explicitly now, I should do that”, and then they’re going to spend untold hours dealing with the consequences of that decision for no good reason.
This goal can sometimes come into conflict with the goal of feeling "lightweight", which in many ways comes down to discouraging programmers from brooding endlessly over details that don't really matter. That could certainly happen with access control, if it were too fine-grained.
he was of course, talking about access control modifiers, but i think a similar concept applies to static typing.
i personally think i tend to fall victim to “overtyping”, which i define as encoding too many assumptions into the type system, because well, you can have too much of any good thing. i wonder if this is something that afflicts many swift developers.
Right - though Java's exception system is informative and does have some upsides, I'm not by any means suggesting we take it as a model for Swift.
Sidenote: Swift does have unchecked exceptions today, essentially, thanks to fatalError and friends. And they're even worse than in Java because not only do you have no idea what code can emit them, you can't even catch them.
It's important that whatever system Swift ends up with can scale gracefully from pedantic to cavalier, to suit the wide variety of target problem domains, from ad-hoc shell scripts to deploy-once embedded apps to server apps etc.
I don't see any red flags here, with the current proposals, though. You can always abstract things away as you wish, e.g. just devolve to throws(URLError) instead of enumerating the exact error cases, or further to throws(any Error). If, when, and to what degree makes sense is absolutely context-dependent.
There is somewhat of a dependency on humans to make sensible error categorisations - and that's limited to what enums are capable of today, basically - so that you can have middle layers of abstraction rather than being forced all the way to any Error. But I think that's viable, even without language aids for composing enums.
A fair concern. This is worth a dedicated discussion section in the final proposal document (call it the "this is why we can't have nice things" section if you're being cynical ).
I like to think that people can - with a little help from the documentation, such as the Swift Language Reference - generally get this right. And it's not like there's any novel problems here - people already have to be mindful about forwards compatibility when it comes to their enums, their types more broadly, their return values, etc. This is just one more place, in defining a function, where those considerations need to be made.
Probably worth noting that for some Swift developers, like myself, there's basically no such thing as a "stable API". A breaking change just means bumping the major version. Code is fluidly trending towards an ideal state, unbeholden to past mistakes. So while I totally understand the concerns about ABI or API compatibility for some contexts, it's important to note that not every Swift user has to deal with those concerns.
In fact, OCaml follows the lead of SML in that thrown errors are entirely dynamically typed — all functions can throw, and the type of caught errors is a single infinitely-extensible exn type.
You don't have to use that feature to report errors, but if you don't, your options are precisely what Swift already offers, i.e. you can have your function return an option or a result.
And in fact this extends to their algebraic effects design too—anything can impose any effect, and an unhandled effect crashes at runtime. So we're actually stricter than OCaml in how we treat effects already.
Would using an opaque throws be supported under the constraints of Embedded Swift? Reading the Embedded Swift pitch, it's not clear if this is possible. If it is, would this be a way to support more flexible evolution of libraries that support Embedded Swift?