- Proposal: SE-NNNN
- Authors: David Owens II, Anton Zhilin
- Status: Pending Approval for Review
- Review manager: TBD
Typed throws
annotation specifies that a function can only throw errors of a certain type:
enum MyError : Error { … }
func foo() throws(MyError) { … }
do {
try foo()
}
catch e { … } // e : MyError
The error handling system within Swift today creates an implicitly loose contract on the API. While this can be desirable in some cases, it’s certainly not desired in all cases. Consider usage of an API with error handling:
enum JSONError : Error { … }
/// Parse JSON from a string
/// - parameter from: The string to parse
/// - Throws: JSONError in case of invalid string format
/// - Returns: An object representing valid JSON
func parseJSON(from: String) throws -> JSON
do {
let json = try parseJSON(from: s)
}
catch e as JSONError { … }
catch { } // ← required
Because thrown errors cannot be stated in function declaration, compiler will give us
a generic Error
in catch
clauses. Thus, a catch-all clause is mandatory for all throwing function calls.
Because we know that we shouldn't ever hit catch-all, it's better to state our intent using fatalError
:
do { … }
catch e as JSONError { … }
catch { fatalError() }
If the API changes error being thrown from JSONError
to ParsingError
, the application will crash.
Compiler won't give us any warning in this case.
In the end, despite many APIs taking on a strict approach to errors, with error types stated in documentation,
all code dealing with error handling is unsafe in the described way.
Add an optional type annotation to throws
clause of function declarations and function types:
enum JSONError : Error { … }
func parseJSON(from: String) throws(JSONError) -> JSON
do {
let json = try parseJSON(from: s)
}
catch e as JSONError { … } // exhaustive; `as JSONError` is extra
// catch e { … } // alternative #1: e is-a JSONError
// catch { … } // alternative #2: error is-a JSONError
// catch JSONError.integerOverflow // alternative #3: match on JSONError
Only one error type can be specified, be it an enum, a struct, or a protocol existential.
This type must conform to Error
protocol.
Plain throws
will still be allowed, and will be equivalent to throws(Error)
.
Generic error types must at least conform to Error
protocol:
func exec<E>(f: () throws(E) -> Void) throws(E)
where E: Error // required
{ try f() }
func exec(f: () throws(MyError) -> Void) rethrows(MyError) { … }
Type, which a function "rethrows", must be a common supertype of all types, which its function parameters "throw".
In the following case, Error1 : Error3
and Error2 : Error3
must be true:
func seq(f: () throws(Error1) -> Void, g: () throws(Error2) -> Void) rethrows(Error3) { … }
Example with generics:
protocol MyBaseError : Error { … }
func seq<E1, E2>(f: () throws(E1) -> Void, g: () throws(E2) -> Void) rethrows(MyBaseError)
where E1: MyBaseError, E2: MyBaseError { … }
In many cases, there won't be a common base type for two or more errors. In these cases, the suggested solution is to resort to unannotated rethrows
:
func seq<E1, E2>(f: () throws(E1) -> Void, g: () throws(E2) -> Void) rethrows
where E1: Error, E2: Error { … }
throws-annotation → throws
throws-annotation → rethrows
throws-annotation → throws
(
type )
throws-annotation → rethrows
(
type )
function-type → attributes opt function-type-argument-clause throws-annotation opt function-result
function-signature → parameter-clause throws-annotation opt function-result opt
protocol-initializer-declaration → initializer-head generic-parameter-clause opt parameter-clause throws-annotation opt generic-where-clause opt
initializer-declaration → initializer-head generic-parameter-clause opt parameter-clause throws-annotation opt generic-where-clause opt initializer-body
closure-throws-annotation → throws
closure-throws-annotation → throws
(
type )
closure-signature → capture-list opt closure-parameter-clause closure-throws-annotation opt function-result opt in
A function type with throws
is subtype of the same function type, but with a wider throws clause. Examples:
protocol A : Error { … }
protocol B : A { … }
protocol C : B { … }
typealias F<E: Error> = () throws(E) -> Void
var x: F<B> = …
var y = x as F<A> // ok, because B: A is true
var z = x as F<C> // error, because B: C is false
var w = x as F<Error> // always ok
Also it’s possible to override functions with the throws
annotations. Covariance on throws
type is allowed.
If multiple throwing calls are in a single do
block, then catch
clauses must cover the set of error types.
Otherwise, "unexhaustive" compilation error is generated. Example:
extension Error1 : BaseError { … }
extension Error2 : BaseError { … }
func foo() throws(Error1)
func bar() throws(Error2)
func buz() throws(Error3)
do {
foo(); bar(); buz()
}
catch e as BaseError { … }
catch e as Error3 { … }
This is a non-breaking change.
In a future proposal, we might want to improve the standard library in a non-breaking way, adding some throws
annotations.
This feature won't touch ABI. It will be a part of type checking and will only exist at compilation stage.
This feature can be removed later without touching ABI.
APIs can only evolve by adding more narrow throws
annotations.
Widening error types or removing type annotations from throws
will break code of clients of those APIs.
Improves incrementally on current throws
syntax. Parametrized annotations are already used in other parts of the language.
() throws(Error) -> Result
This version feels lighter. The major disadvantage is visual ambiguity: does the function throw Error -> Result
type?
() throws Error -> Result
This version mimics Either
type from other languages.
() -> Result throws Error
It gets tricky with curried functions:
() -> (() -> Result) throws Error
In this version, the concept of throwing is based on type unions, or implicit enums A | B
.
() -> Result | Error
This may seem nice and minimalistic from theoretical point of view, but on practise |
does not mean "error handling" at all.
Additionally, type unions have been explicitly rejected.
Current proposal is intended to be minimalistic and keep controversal features out as much as possible.
Typed throws
can be extended in a series of future proposals.
func getPreferences() throws(FileNotFoundError, ParseError) -> Preferences { … }
Some say that this change would bring in Java-madness, where lists of passing-through errors grow very fast and become meaningless.
Error
protocol will be replaced with Any
, because making all errors conform to an marker protocol seems redundant.
This will be a breaking change, so it was excluded from current proposal.
Because throws
≡ throws(Error)
or throws(Any)
, plain throws
seems redundant.
This will be a breaking change, so it was excluded from current proposal.
In generic context, this will allow to replace rethrows
to some extent:
func exec<E>(f: () throws(E) -> Void) throws(E) { … }
Here, if E = Never
, then we get non-throwing version of exec
.
Therefore, we can remove rethrows
from the language.
From the earlier threads on the swift-evolution mailing list, there are a few primary points of contention about this proposal.
No. The primary reason is that a function can only return a single error-type. This already greatly reduces the deep class-based, exception-type model to a single, polymorphic error type (for class-based ErrorType
implementations). Swift also takes a different model than Java; this was mostly laid out here: Error Handling Rationale. But briefly, many of the numerous exceptions that are thrown in Java are of the "Universal Error" classification, which Swift's error model doesn't handle.
Potentially, yes. This depends on how the ABI is handled in Swift 3 for enums. The same problem exists today, although at a lesser extent, for any API that returns an enum today.
Chris Lattner mentioned this on the thread:
The resilience model addresses how the public API from a module can evolve without breaking clients (either at the source level or ABI level). Notably, we want the ability to be able to add enum cases to something by default, but also to allow API authors to opt into more performance/strictness by saying that a public enum is “fragile” or “closed for evolution”.
So if enums have an attribute that allows API authors to denote the fragility enums, then this can be handled via that route.
Another potential fix is that only internal
and private
scoped functions are allowed to use the exhaustive-style catch-clauses. For all public
APIs, they would still need the catch-all clauses.
For APIs that return non-enum based ErrorType
implementations, then no, this does not contribute to the fragility problem.
This is a philosophical debate. I’ll simply state that I believe that simply re-throwing an error, say some type of IO error, from your API that is not an IO-based API is design flaw: you are exposing implementation details to users. This creates a fragile API surface.
Also, since the type annotation is opt-in, I feel like this is a really minor argument. If your function is really able to throw errors from various different API calls, then just stick with the default ErrorType
.
However, it is the case that if you do wish to propogate the errors out, then yes, you need to create wrappers. The Rust language does this today as well.
To clear, it's not because of “Java checked exceptions” (as it might be inferred because of the defense to Java's checked exceptions). Rather, it’s because nowhere else in the language are types allowed to be essentially annotated in a sum-like fashion. We can’t directly say a function returns an Int or a String. We can’t say a parameter can take an Int or a Double. Similarly, I propose we can’t say a function can return an error A or B.
Thus, the primary reason is about type-system consistency.
Swift already supports a construct to create sum types: associated enums. What it doesn’t allow is the ability to create them in a syntactic shorthand. In this way, my error proposal does the same thing as Rust: multiple return types need to be combined into a single-type - enum.
If Swift is updated to allow the creation of sum-types and use them as qualifiers for type declarations, then I don't see how they wouldn't simply fall inline here as well.