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?
Due to the large number of posts split between different threads I'm confused as to whether concrete error types will be allowed, e.g.
func a() throws(MyError) {}
do {
a()
} catch(error) {
// Is this allowed or will `error` be of type `any Error`?
error as MyError
}
And if concrete errors are allowed, what's the strategy to prevent the proliferation of precise error typing over the sensible default of type-erased errors? Also, will embedded platforms retain Standard Swift's type-erased errors and then aggressively inline, or will it exclusively use concrete error types to avoid dynamic casting?
For libraries that want to provide a consistent API across embedded and full-featured Swift targets, I wonder if throws(some Error) could be a way to minimize the API difference between targets. Outwardly that would give callers about the same amount of information they're allowed to code against as throws(any Error), allowing for API evolution to change the error type under the hood, though on embedded platforms functions would still be restricted to throwing a single concrete underlying type.
Doesn’t Error being self-conforming make some Error fairly equivalent to any Error? In other words, why not make throws(some Error) the fully expanded spelling of “untyped” errors, and ban throws(any Error)?
Then I guess the next question is for @Douglas_Gregor and @John_McCall: does throws(some Error) solve all the embedded-specific needs without introducing the ergonomic pitfalls of errors with exposed types?
Even in "full" Swift I wouldn't jump to the conclusion that it's a drop-in replacement for untyped throws, or that supporting throws(some Error) is less work than supporting typed throws in the general case. The opaque type still represents some concrete type, tied to a specific declaration's underlying implementation, and as soon as you use it as a closure or through some other abstraction we would need to either propagate that opaque type or erase it to any Error, and you would get type inequalities between different declarations' thrown errors like you do with other opaque return types.