- Proposal: SE-0413
- Authors: Jorge Revuelta (@minuscorp), Torsten Lehmann, Doug Gregor
- Review Manager: Steve Canon
- Status: Implemented (Swift 6.0)
- Review: latest pitch, review, acceptance
Swift's error handling model allows functions and closures marked throws
to note that they can exit by throwing an error. The error values themselves are always type-erased to any Error
. This approach encourages errors to be handled generically, and remains a good default for most code. However, there are some places where the type erasure is unfortunate, because it doesn't allow for more precise error typing in narrow places where it is possible and desirable to handle all errors, or where the costs of type erasure are prohibitive.
This proposal introduces the ability to specify that functions and closures only throw errors of a particular concrete type.
Note: the originally accepted version of this proposal included type inference changes intended for Swift 6.0 that were behind the upcoming feature flag
FullTypedThrows
. These type inference changes did not get implemented in Swift 6.0, and have therefore been removed from this proposal and placed into "Future Directions" so they can be revisited once implemented.
- Introduction
- Motivation
- Proposed solution
- Detailed design
- Source compatibility
- Effect on API resilience
- Effect on ABI stability
- Future directions
- Alternatives considered
- Revision history
Swift is known for being explicit about semantics and using types to communicate constraints that apply to specific APIs. From that perspective, the fact that all thrown errors are of type any Error
feels like an outlier. However, it reflects the view laid out in the original error handling rationale that errors are generally propagated and rendered, but rarely handled exhaustively, and are prone to changing over time in a way that types are not.
The desire to provide specific thrown error types has come up repeatedly on the Swift forums. Here are just a few of the forum threads calling for some form of typed throws:
- [Pitch N+1] Typed throws
- Typed throw functions
- Status check: typed throws
- Precise error typing in Swift
- Typed throws
- [Pitch] Typed throws
- Type-annotated throws
- Proposal: Allow Type Annotations on Throws
- Proposal: Allow Type Annotations on Throws
- Proposal: Typed throws
- Type Inferencing For Error Handling (try catch blocks)
In a sense, Swift started down the path toward typed throws with the introduction of the Result
type in the standard library, which captured a specific thrown error type in its Failure
parameter. That pattern was replicated in the Task
type and other concurrency APIs. The loss of information between types like Result
and Task
and the language's error-handling system provides partial motivation for the introduction of typed throws, and is discussed further below.
Typed throws also provides benefits in places where clients need to exhaustively handle errors. For this to make sense, the set of potential failure conditions must be relatively fixed, either because they come from the same module or package as the clients, or because they come from a library that is effectively standalone and unlikely to evolve to (e.g.) pass through an error from another lower-level library. Typed throws also provides benefits in generic code that will propagate errors from its arguments, but never generate errors itself, as a more flexible alternative to the existing rethrows
. Finally, typed throws also open up the potential for more efficient code, because they avoid the overhead associated with existential types (any Error
).
Even with the introduction of typed throws into Swift, the existing (untyped) throws
remains the better default error-handling mechanism for most Swift code. The section "When to use typed throws" describes the circumstances in which typed throws should be used.
Assume you have this Error type
enum CatError: Error {
case sleeps
case sitsAtATree
}
Compare
func callCat() -> Result<Cat, CatError>
or
func callFutureCat() -> Task<Cat, CatError>
with
func callCatOrThrow() throws -> Cat
throws
communicates less information about why the cat is not about to come to you.
The fact thatthrows
carries less information than Result
or Task
means that conversions to throws
loses type information, which can only be recovered by explicit casting:
func callAndFeedCat1() -> Result<Cat, CatError> {
do {
return Result.success(try callCatOrThrow())
} catch {
// won't compile, because error type guarantee is missing in the first place
return Result.failure(error)
}
}
func callAndFeedCat2() -> Result<Cat, CatError> {
do {
return Result.success(try callCatOrThrow())
} catch let error as CatError {
// compiles
return Result.failure(error)
} catch {
// won't compile, because exhaustiveness can't be checked by the compiler
// so what should we return here?
return Result.failure(error)
}
}
Using explicit errors with Result
has major implications for a code base. Because the exception handling mechanism ("goto catch") is not built into the language (like throws
), you need to do that on your own, mixing the exception handling mechanism with domain logic.
If you use Result
in a functional (i.e. monadic) way, you need extensive use of map
, flatMap
and similar operators.
Example is taken from Question/Idea: Improving explicit error handling in Swift (with enum operations) - Using Swift - Swift Forums.
struct SimpleError: Error {
let message: String
}
struct User {
let firstName: String
let lastName: String
}
func stringResultFromArray(_ array: [String], at index: Int, errorMessage: String) -> Result<String, SimpleError> {
guard array.indices.contains(index) else { return Result.failure(SimpleError(message: errorMessage)) }
return Result.success(array[index])
}
func userResultFromStrings(strings: [String]) -> Result<User, SimpleError> {
return stringResultFromArray(strings, at: 0, errorMessage: "Missing first name")
.flatMap { firstName in
stringResultFromArray(strings, at: 1, errorMessage: "Missing last name")
.flatMap { lastName in
return Result.success(User(firstName: firstName, lastName: lastName))
}
}
}
That's the functional way of writing exceptions, but Swift does not provide enough functional constructs to handle that comfortably (compare with Haskell/do notation).
We can also just unwrap every result by switching over it and wrapping the value or error into a result again.
func userResultFromStrings(strings: [String]) -> Result<User, SimpleError> {
let firstNameResult = stringResultFromArray(strings, at: 0, errorMessage: "Missing first name")
switch firstNameResult {
case .success(let firstName):
let lastNameResult = stringResultFromArray(strings, at: 1, errorMessage: "Missing last name")
switch lastNameResult {
case .success(let lastName):
return Result.success(User(firstName: firstName, lastName: lastName))
case .failure(let simpleError):
return Result.failure(simpleError)
}
case .failure(let simpleError):
return Result.failure(simpleError)
}
}
This is even more boilerplate than the first approach, because now we are writing the implementation of the flatMap
operator over and over again.
Untyped errors have the existential type any Error
, which incurs some necessary overhead, in code size, heap allocation overhead, and execution performance, due to the need to support values of unknown type. In constrained environments such as those supported by Embedded Swift, existential types may not be permitted due to these overheads, making the existing untyped throws mechanism unusable in those environments.
In general, we want to add the possibility of using throws
with a single, specific error type.
func callCat() throws(CatError) -> Cat {
if Int.random(in: 0..<24) < 20 {
throw .sleeps
}
// ...
}
The function can only throw instances of CatError
. This provides contextual type information for all throw sites, so we can write .sleeps
instead of the more verbose CatError.sleeps
that's needed with untyped throws. Any attempt to throw any other kind of error out of the function will be an error:
func callCatBadly() throws(CatError) -> Cat {
throw SimpleError(message: "sleeping") // error: SimpleError cannot be converted to CatError
}
Maintaining specific error types throughout a function is much easier than when using Result
, because one can use try
consistently:
func stringFromArray(_ array: [String], at index: Int, errorMessage: String) throws(SimpleError) -> String {
guard array.indices.contains(index) else { throw SimpleError(message: errorMessage) }
return array[index]
}
func userResultFromStrings(strings: [String]) throws(SimpleError) -> User {
let firstName = try stringFromArray(strings, at: 0, errorMessage: "Missing first name")
let lastName = try stringFromArray(strings, at: 1, errorMessage: "Missing last name")
return User(firstName: firstName, lastName: lastName)
}
The error handling mechanism is pushed aside and you can see the domain logic more clearly.
With typed throws, a throwing function contains the same information about the error type as Result
, making it easier to convert between the two:
func callAndFeedCat1() -> Result<Cat, CatError> {
do {
return Result.success(try callCat())
} catch {
// would compile now, because error is `CatError`
return Result.failure(error)
}
}
Note that the implicit error
variable within the catch block is inferred to the concrete type CatError
; there is no need for the existential any Error
.
When a do
statement can throw errors with different concrete types, or involves any calls to functions using untyped throws, the catch
block will receive a thrown error type of an any Error
type:
func callKids() throws(KidError) -> [Kid] { ... }
do {
try callCat()
try callKids()
} catch {
// error has type 'any Error', as it does today
}
The caught error type for a do..catch
statement will be inferred from the various throwing sites within the body of the do
block. One can explicitly specify this type with a throws
clause on do
block itself, i.e.,
do throws(CatError) {
if isDaylight && foodBowl.isEmpty {
throw .sleeps // equivalent to CatError.sleeps
}
try callCat()
} catch let myError {
// myError is of type CatError
}
When one needs to translate errors of one concrete type to another, use a do...catch
block around each sequence of calls that produce the same kind of error :
func firstNameResultFromArray(_ array: [String]) throws(FirstNameError) -> String {
guard array.indices.contains(0) else { throw FirstNameError() }
return array[0]
}
func userResultFromStrings(strings: [String]) throws(SimpleError) -> User {
do {
let firstName = try firstNameResultFromArray(strings)
return User(firstName: firstName, lastName: "")
} catch {
// error is a `FirstNameError`, map it to a `SimpleError`.
throw SimpleError(message: "Missing first name")
}
}
Typed throws generalizes over both untyped throws and non-throwing functions. A function specified with any Error
as its thrown type:
func throwsAnything() throws(any Error) { ... }
is equivalent to untyped throws:
func throwsAnything() throws { ... }
Similarly, a function specified with Never
as its thrown type:
func throwsNothing() throws(Never) { ... }
is equivalent to a non-throwing function:
func throwsNothing() { }
There is a more general subtyping rule here that says that you can loosen the thrown type, i.e., converting a non-throwing function to a throwing one, or a function that throws a concrete type to one that throws any Error
.
The ability to throw a generic error parameter that might be Never
allows one to safely express some rethrowing patterns that are otherwise not possible with rethrows. For example, consider a function that semantically rethrows, but needs to do so by going through some code that doesn't throw:
/// Count number of nodes in the tree that match a particular predicate
func countNodes(in tree: Node, matching predicate: (Node) throws -> Bool) rethrows -> Int {
class MyNodeVisitor: NodeVisitor {
var error: (any Error)? = nil
var count: Int = 0
var predicate: (Node) throws -> Bool
init(predicate: @escaping (Node) throws -> Bool) {
self.predicate = predicate
}
override func visit(node: Node) {
do {
if try predicate(node) {
count = count + 1
}
} catch let localError {
error = error ?? localError
}
}
}
return try withoutActuallyEscaping(predicate) { predicate in
let visitor = MyNodeVisitor(predicate: predicate)
visitor.visitTree(node)
if let error = visitor.error {
throw error // error: is not throwing as a consequence of 'predicate' throwing.
} else {
return visitor.count
}
}
}
Walking through the code, we can convince ourselves that MyNodeVisitor.error
will only ever be set as a result of the predicate throwing an error, so this code semantically fulfills the contract of rethrows
. However, the Swift compiler's rethrows checking cannot perform such an analysis, so it will reject this function. The limitation on rethrows
has prompted at least two pitches to add an "unsafe" or "unchecked" rethrows variant, turning this into a runtime-checked contract.
Typed throws offer a compelling alternative: one can capture the error type of the closure argument in a generic parameter, and use that consistently throughout. This is immediately useful for maintaining precise typed error information in generic code that only rethrows the error from its closure arguments, like map
:
extension Collection {
func map<U, E: Error>(body: (Element) throws(E) -> U) throws(E) -> [U] {
var result: [U] = []
for element in self {
result.append(try body(element))
}
return result
}
}
When given a closure that throws CatError
, this formulation of map
will throw CatError
. When given a closure that doesn't throw, E
will be Never
, so map
is non-throwing.
This approach extends to our countNodes
example:
/// Count number of nodes in the tree that match a particular predicate
func countNodes<E: Error>(in tree: Node, matching predicate: (Node) throws(E) -> Bool) throws(E) -> Int {
class MyNodeVisitor<E>: NodeVisitor {
var error: E? = nil
var count: Int = 0
var predicate: (Node) throws(E) -> Bool
init(predicate: @escaping (Node) throws(E) -> Bool) {
self.predicate = predicate
}
override func visit(node: Node) {
do {
if try predicate(node) {
count = count + 1
}
} catch let localError {
error = error ?? localError // okay, error has type E?, localError has type E
}
}
}
return try withoutActuallyEscaping(predicate) { predicate in
let visitor = MyNodeVisitor(predicate: predicate)
visitor.visitTree(node)
if let error = visitor.error {
throw error // okay! error has type E, which can be thrown out of this function
} else {
return visitor.count
}
}
}
Note that typed throws has elegantly solved our problem, because any throwing site that throws a value of type E
is accepted. When the closure argument doesn't throw, E
is inferred to Never
, and (dynamically) no instance of it will ever be created.
Typed throws makes it possible to strictly specify the thrown error type of a function, but doing so constrains the evolution of that function's implementation. Additionally, errors are usually propagated or rendered, but not exhaustively handled, so even with the addition of typed throws to Swift, untyped throws
is better for most scenarios. Consider typed throws only in the following circumstances:
- In code that stays within a module or package where you always want to handle the error, so it's purely an implementation detail and it is plausible to handle the error.
- In generic code that never produces its own errors, but only passes through errors that come from user components. The standard library contains a number of constructs like this, whether they are
rethrows
functions likemap
or are capturing aFailure
type like inTask
orResult
. - In dependency-free code that is meant to be used in a constrained environment (e.g., Embedded Swift) or cannot allocate memory, and will only ever produce its own errors.
Resist the temptation to use typed throws because there is only a single kind of error that the implementation can throw. For example, consider an operation that loads bytes from a specified file:
public func loadBytes(from file: String) async throws(FileSystemError) -> [UInt8] // should use untyped throws
Internally, it is using some file system library that throws a FileSystemError
, which it then republishes directly. However, the fact that the error was specified to always be a FileSystemError
may hamper further evolution of this API: for example, it might be reasonable for this API to start supporting loading bytes from other sources (say, a network connection or database) when the file name matches some other schema. However, errors from those other libraries will not be FileSystemError
instances, which poses a problem for loadBytes(from:)
: it either needs to translate the errors from other libraries into FileSystemError
(if that's even possible), or it needs to break its API contract by adopting a more general error type (or untyped throws
).
This section will be added to the Swift API Design Guidelines.
The Swift grammar is updated wherever there is either throws
or rethrows
, to optionally include a thrown type, e.g.,
throws-clause -> throws thrown-type(opt)
thrown-type -> '(' type ')'
Changing from
function-type → attributes(opt) function-type-argument-clause async(opt) throws(opt) -> type
to
function-type → attributes(opt) function-type-argument-clause async(opt) throws-clause(opt) -> type
Examples
() -> Bool
() throws -> Bool
() throws(CatError) -> Bool
Changing from
closure-signature → capture-list(opt) closure-parameter-clause async(opt) throws(opt) function-result opt in
to
closure-signature → capture-list(opt) closure-parameter-clause async(opt) throws-clause(opt) function-result opt in
Examples
{ () -> Bool in true }
{ () throws -> Bool in true }
{ () throws(CatError) -> Bool in true }
Changing from
function-signature → parameter-clause async(opt) throws(opt) function-result(opt)
function-signature → parameter-clause async(opt) rethrows(opt) function-result(opt)
initializer-declaration → initializer-head generic-parameter-clause(opt) parameter-clause async(opt) throws(opt)
initializer-declaration → initializer-head generic-parameter-clause(opt) parameter-clause async(opt) throws(opt)
to
function-signature → parameter-clause async(opt) throws-clause(opt) function-result(opt)
initializer-declaration → initializer-head generic-parameter-clause(opt) parameter-clause async(opt) throws-clause(opt)
Note that the current grammar does not account for throwing accessors, although they should receive the same transformation.
The syntax of a do..catch
block is extended with an optional throw clause:
do-statement → do throws-clause(opt) code-block catch-clauses?
If a throws-clause
is present, then there must be at least one catch-clause
.
func callCat() -> Cat
func callCat() throws -> Cat
func callCat() throws(CatError) -> Cat
init()
init() throws
init() throws(CatError)
var value: Success {
get throws(Failure) { ... }
}
Any function, closure or function type that is marked as throws
can declare which type the function throws. That type, which is called the thrown error type, must conform to the Error
protocol.
Every uncaught error that can be thrown from the body of the function must be convertible to the thrown error type. This applies to both explicit throw
statements and any errors thrown by other calls (as indicated by a try
). For example:
func throwingTypedErrors() throws(CatError) {
throw CatError.asleep // okay, type matches
throw .asleep // okay, can infer contextual type from the thrown error type
throw KidError() // error: KidError is not convertible to CatError
try callCat() // okay
try callKids() // error: throws KidError, which is not convertible to CatError
do {
try callKids() // okay, because this error is caught and suppressed below
} catch {
// eat the error
}
}
Because a value of any Error
-conforming type implicitly converts to any Error
, this implies that an function declared with untyped throws
can throw anything:
func untypedThrows() throws {
throw CatError.asleep // okay, CatError converts to any Error
throw KidError() // okay, KidError converts to any Error
try callCat() // okay, thrown CatError converts to any Error
try callKids() // okay, thrown KidError converts to any Error
}
Therefore, these rules subsume those of untyped throws, and no existing code will change behavior.
Note that the constraint that the thrown error type must conform to Error
means that one cannot use an existential type such as any Error & Codable
as the thrown error type:
// error: any Error & Codable does not conform to Error
func remoteCall(function: String) async throws(any Error & Codable) -> String { ... }
The any Error
existential has special semantics that allow it to conform to the Error
protocol, introduced along with Result
. A separate language change would be required to allow other existential types to conform to the Error
protocol.
A do...catch
block is used to catch and process thrown errors. With only untyped errors, the type of the error thrown from inside the do
block is always any Error
. In the presence of typed throws, the type of the error thrown from inside the do
block can either be explicitly specified with a throws
clause following the do
, or inferred from the specific throwing sites.
When the do
block specifies a thrown error type, that error type can be used for inferring the contextual type of throw
statements. For example:
do throws(CatError) {
if isDaytime && foodBowl.isEmpty {
throw .sleep
}
} catch {
// implicit 'error' value has type CatError
}
As with other uses of untyped throws, do throws
is equivalent to do throws(any Error)
.
When there is no throws clause, the thrown error type is inferred from the body of the do
block. When all throwing sites within a do
block produce the same error type (ignoring any that throw Never
), that error type is used as the type of the thrown error. For example:
do /*infers throws(CatError)*/ {
try callCat() // throws CatError
if something {
throw CatError.asleep // throws CatError
}
} catch {
// implicit 'error' value has type CatError
if error == .asleep {
openFoodCan()
}
}
This also implies that one can use the thrown type context to perform type-specific checks in the catch clauses, e.g.,
do /*infers throws(CatError)*/ {
try callCat() // throws CatError
if something {
throw CatError.asleep // throws CatError
}
} catch .asleep {
openFoodCan()
} // note: CatError can be thrown out of this do...catch block when the cat isn't asleep
Rationale: By inferring a concrete result type for the thrown error type, we can entirely avoid having to reason about existential error types within
catch
blocks, leading to a simpler syntax. Additionally, it preserves the notion that ado...catch
block that has acatch
site accepting anything (i.e., one with no conditions) can exhaustively suppress all errors.
When throw sites within the do
block throw different (non-Never
) error types, the inferred error type is any Error
. For example:
do /*infers throws(any Error)*/ {
try callCat() // throws CatError
try callKids() // throw KidError
} catch {
// implicit 'error' variable has type 'any Error'
}
In essence, when there are multiple possible thrown error types, we immediately resolve to the untyped equivalent of any Error
. We will refer to this notion as a type function errorUnion(E1, E2, ..., EN)
, which takes N
different error types (e.g., for throwing sites within a do
block) and produces the union error type of those types. Our definition and use of errorUnion
for typed throws subsumes the existing rule for untyped throws, in which every throw site produces an error of type any Error
.
Rationale: While it would be possible to compute a more precise "union" type of different error types, doing so is potentially an expensive operation at compile time and run time, as well as being harder for the programmer to reason about. If in the future it becomes important to tighten up the error types, that could be done in a mostly source-compatible manner.
The semantics specified here are not fully source compatible with existing Swift code. A do...catch
block that contains throw
statements of a single concrete type (and no other throwing sites) might depend on the error being caught as any Error
. Here is a contrived example:
do /*infers throws(CatError) in Swift 6 */ {
throw CatError.asleep
} catch {
var e = error // currently has type any Error, will have type CatError
e = KidsError() // currently well-formed, will become an error
}
To prevent this source compatibility issue, we refine the rule slightly to specify that any throw
statement always throws a value of type any Error
. That way, one can only get a caught error type more specific than any Error
when the both of the do..catch
contains no throw
statements and all of the try
operations are using functions that make use of typed throws.
Note that the only way to write an exhaustive do...catch
statement is to have an unconditional catch
block. The dynamic checking provided by is
or as
patterns in the catch
block cannot be used to make a catch exhaustive, even if the type specified is the same as the type thrown from the body of the do
:
func f() {
do /*infers throws(CatError)*/ {
try callCat()
} catch let ce as CatError {
} // error: do...catch is not exhaustive, so this code rethrows CatError and is ill-formed
}
Note: Exhaustiveness checking in the general is expensive at compile time, and the existing language uses the presence of an unconditional
catch
block as the indicator for an exhaustivedo...catch
. See the section on closure thrown type inference for more details about inferring throwing closures.
A function marked rethrows
throws only when one of its closure parameters throws. It is typically used with higher-order functions, such as the map
operation on a collection:
extension Collection {
func map<U>(body: (Element) throws -> U) rethrows -> [U] {
var result: [U] = []
for element in self {
result.append(try body(element))
}
return result
}
}
When provided with a throwing closure, map
can throw, and it chooses to directly throw the same error as the body. This contract can be more precisely modeled using typed throws:
extension Collection {
func map<U, E: Error>(body: (Element) throws(E) -> U) throws(E) -> [U] {
var result: [U] = []
for element in self {
result.append(try body(element))
}
return result
}
}
Now, when map
is provided with a closure that throws E
, it can only throw an E
. For a non-throwing closure, E
will be Never
and map
is non-throwing. For an untyped throwing closure, E
will be any Error
and we get the same type-level behavior as the rethrows
version of map
.
However, because rethrows
uses untyped errors, map
would be permitted to substitute a different error type that, for example, provides more information about the failing element:
struct MapError<Element>: Error {
var failedElement: Element
var underlyingError: any Error
}
extension Collection {
func map<U>(body: (Element) throws -> U) rethrows -> [U] {
var result: [U] = []
for element in self {
do {
result.append(try body(element))
} catch {
// Provide more information about the failure
throw MapError(failedElement: element, underlyingError: error)
}
}
return result
}
}
Typed throws, as presented here, is not able to express the contract of this function.
The Swift standard library does not perform error substitution of this form, and its contract for operations like map
is best expressed by typed throws as shown above. It is likely that many existing rethrows
functions are better expressed with typed throws. However, not all rethrows
functions can be expressed by typed throws, if they are performing error substitution like this last map
.
Therefore, this proposal does not change the primary semantics of rethrows
: it remains untyped, and it is ill-formed to attempt to provide a thrown error type to a rethrows
function. The Alternatives Considered section provides several options for rethrows
, which can become the subject of a future proposal.
However, there is a small change in the type checking behavior of a rethrows
function to improve source compatibility in certain cases. Specifically, consider a rethrows
function that calls into a function with typed throws:
extension Collection {
func filter<E: Error>(_ isIncluded: (Element) throws(E) -> Bool) throws(E) -> [Element] { ... }
func filterOdds<E: Error>(_ isIncluded: (Element) throws -> Bool) rethrows -> [Element {
var onOdd = true
return try filter { element in
defer { onOdd = !onOdd }
return onOdd && isIncluded(element)
} // error: call to filter isn't "rethrows"
}
}
The standard rethrows
checking rejects the call to filter
because, technically, it could throw any Error
under any circumstances. Unfortunately, this behavior is a source compatibility problem for the standard library's adoption of typed throws, because an existing rethrows
function calling into something like map
or filter
would be rejected once those introduce typed throws. This proposal introduces a small compatibility feature that considers a function that
- Has a thrown error type that is a generic parameter (call it
E
) of the function itself, - Has no protocol requirements on
E
other than that it conform to theError
protocol, and - Any parameters of throwing function type throw the specific error type
E
.
to be a rethrowing function for the purposes of rethrows
checking in its caller. This compatibility feature introduces a small soundness hole in rethrows
functions that can only be removed with improvements to type inference behavior.
The thrown error type of a function can be specified with an opaque result type. For example:
func doSomething() throws(some Error) { ... }
The opaque thrown error type is like a result type, so the concrete type of the error is chosen by the doSomething
function itself, and could change from one version to the next. The caller only knows that the error type conforms to the Error
protocol; the concrete type won't be knowable until runtime.
Opaque result types can be used as an alternative to existentials (any Error
) when there is a fixed number of potential error types that might be thrown , and we either can't (due to being in an embedded environment) or don't want to (for performance or code-evolution reasons) expose the precise error type. For example, one could use a suitable Either
type under the hood:
func doSomething() throws(some Error) {
do {
try callCat()
} catch {
throw Either<CatError, KidError>.left(error)
}
do {
try callKids()
} catch {
throw Either<CatError, KidError>.right(error)
}
}
Due to the contravariance of parameters, an opaque thrown error type that occurs within a function parameter will be an opaque parameter. This means that the closure argument itself will choose the type, so
func map<T>(_ transform: (Element) throws(some Error) -> T) rethrows -> [T]
is equivalent to
func map<T, E: Error>(_ transform: (Element) throws(E) -> T) rethrows -> [T]
An async let
initializer can throw an error, and that error is effectively rethrown at any point where one of the variables defined in the async let
is referenced. For example:
async let answer = callCat()
// ...
try await answer // could rethrow the result from the initializer here
The type thrown by the variables of an async let
is determined using the same rules as for the do
part of a do...catch
block. In the example above, accesses to answer
can throw an error of type CatError
.
A function type that throws an error of type A
is a subtype of a function type that differs only in that it throws an error of type B
when A
is a subtype of B
. As previously noted, a throws
function that does not specify the thrown error type will have a thrown type of any Error
, and a non-throwing function has a thrown error type of Never
. For subtyping purposes, Never
is assumed to be a subtype of all error types.
The subtyping rule manifests in a number of places, including function conversions, protocol conformance checking and refinements, and override checking, all of which are described below.
Having related errors and a non-throwing function
class BaseError: Error {}
class SubError: BaseError {}
let f1: () -> Void
Converting a non-throwing function to a throwing one is allowed
let f2: () throws(SubError) -> Void = f1
It's also allowed to assign a subtype of a thrown error, though the subtype information is erased and the error of f2 will be casted up.
let f3: () throws(BaseError) -> Void = f2
Erasing the specific error type is possible
let f4: () throws -> Void = f3
Protocols should have the possibility to conform and refine other protocols containing throwing functions based on the subtype relationship of their functions. This way it would be possible to throw a more specialised error or don't throw an error at all.
protocol Throwing {
func f() throws
}
struct ConcreteNotThrowing: Throwing {
func f() { } // okay, doesn't have to throw
}
enum SpecificError: Error { ... }
struct ConcreteThrowingSpecific: Throwing {
func f() throws(SpecificError) { } // okay, throws a specific error
}
A declaration in a subclass that overrides a superclass declaration can be a subtype of the superclass declaration, for example:
class BlueError: Error { ... }
class DeepBlueError: BlueError { ... }
class Superclass {
func f() throws { }
func g() throws(BlueError) { }
}
class Subclass: Superclass {
override func f() throws(BlueError) { } // okay
override func g() throws(DeepBlueError) { } // okay
}
class Subsubclass: Subclass {
override func f() { } // okay
override func g() { } // okay
}
The type checker can infer thrown error types in a number of different places, making it easier to carry specific thrown type information through a program without additional annotation. This section covers the various ways in which thrown errors interact with type inference.
An associated type can be used as the thrown error type in other protocol requirements. For example:
protocol CatFeeder {
associatedtype FeedError: Error
func feedCat() throws(FeedError) -> CatStatus
}
When a concrete type conforms to such a protocol, the associated type can be inferred from the declarations that satisfy requirements that mention the associated type in a typed throws clause. For the purposes of this inference, a non-throwing function has Never
as its error type and an untyped throws
function has any Error
as its error type. For example:
struct Tabby: CatFeeder {
func feedCat() throws(CatError) -> CatStatus { ... } // okay, FeedError is inferred to CatError
}
struct Sphynx: CatFeeder {
func feedCat() throws -> CatStatus { ... } // okay, FeedError is inferred to any Error
}
struct Ragdoll: CatFeeder {
func feedCat() -> CatStatus { ... } // okay, FeedError is inferred to Never
}
When a function signature uses a generic parameter or associated type as a thrown type, that generic parameter or associated type is implicitly inferred to conform to the Error
type. For example, given this declaration for map
:
func map<T, E>(body: (Element) throws(E) -> T) throws(E) { ... }
the function has an inferred requirement E: Error
.
Result
's init(catching:) operation translates a throwing closure into a Result
instance. It's currently defined only when the Failure
type is any Error
, i.e.,
init(catching body: () throws -> Success) where Failure == any Error { ... }
Replace this with an initializer that uses typed throws:
init(catching body: () throws(Failure) -> Success)
The new initializer is more flexible: in addition to retaining the error type from typed throws, it also supports non-throwing closure arguments by inferring Failure
to be equal to Never
.
Additionally, Result
's get()
operation:
func get() throws -> Success
should use Failure
as the thrown error type:
func get() throws(Failure) -> Success
The standard library contains a large number of operations that rethrow
. In all cases, the standard library will only throw from a call to one of the closure arguments: it will never substitute a different thrown error. Therefore, each rethrows
operation in the standard library should be replaced with one that uses typed throws to propagate the same error type. For example, the Optional.map
operation would change from:
public func map<U>(
_ transform: (Wrapped) throws -> U
) rethrows -> U?
to
public func map<U, E>(
_ transform: (Wrapped) throws(E) -> U
) throws(E) -> U?
This is a mechanical transformation that is applied throughout the standard library.
This proposal has called out a few specific places where the introduction of typed throws into the language could affect source compatibility. However, in those places, we have opted for semantics that ensure that existing Swift code that does not change behavior, to make this proposal act as a purely additive change to the language. Once a function adopts typed throws, the effect of typed throws can then ripple to its callers.
An API that uses typed throws cannot make its thrown error type more general (or untyped) without breaking existing clients that depend on the specific thrown error type:
// Library
public enum DataLoaderError {
case missing
}
public class DataLoader {
func load() throws(DataLoaderError) -> Data { ... }
}
// Client code
func processError(_ error: DataLoaderError) { ... }
func load(from dataLoader: dataLoader) {
do {
try dataLoader.load()
} catch {
processError(error)
}
}
Any attempt to generalize the thrown type of DataLoader.load()
will break the client code, which depends on getting a DataLoaderError
in the catch
block.
Going in the other direction, of making the thrown error type more specific than it used to be (or adopting typed throws in an API that previously used untyped throws) can also break clients, but in much more limited cases. For example, let's consider the same API above, but in reverse:
// Library
public enum DataLoaderError {
case missing
}
public class DataLoader {
func load() throws -> Data { ... }
}
// Client
func processError(_ error: any Error) { ... }
func load(from dataLoader: dataLoader) {
do {
try dataLoader.load()
} catch {
processError(error)
}
}
Here, the DataLoader.load()
function could be updated to throw DataLoaderError
and this particular client code would still work, because DataLoaderError
is convertible to any Error
. Note that clients could still be broken by this kind of change, for example overrides of an open
function, declarations that satisfy a protocol requirement, or code that relies on the precide error type (say, by overloading). However, such a change is far less likely to break clients of an API than loosening thrown type informance.
A rethrows
function can generally be replaced with a function that is generic over the thrown error type of its closure argument and propagates that thrown error. For example, one can replace this API:
public func last(
where predicate: (Element) throws -> Bool
) rethrows -> Element?
with
public func last<E>(
where predicate: (Element) throws(E) -> Bool
) throws(E) -> Element?
When calling this function, the closure argument supplies the thrown error type (E
), which can also be inferred to any Error
(for untyped throws
) or Never
(for non-throwing functions). Existing clients of this new function therefore see the same behavior as with the rethrows
version.
There is one difference between the two functions that could break client code that is referring to such functions without calling them. For example, consider the following code:
let primes = [2, 3, 5, 7]
let getLast = primes.last(where:)
With the rethrows
formulation of the last(where:)
function, getLast
will have the type ((Int) throws -> Bool) throws -> Int?
. With the typed-errors formulation, this code will result in an error because the an argument for the generic parameter E
cannot be inferred without context. Note that this is only a problem when there is no context type for getLast
, and can be fixed by providing it with a type:
let getLast: ((Int) -> Bool) -> Int? = primes.last(where:) // okay, E is inferred to Never
Note that one would have to do the same thing with the rethrows
formulation to produce a non-throwing getLast
, because rethrows
is not a part of the formal type system. Given that most rethrows
operations are already generic in other parameters (unlike last(where:)
), and most uses of such APIs are either calls or have type context, it is expected that the actual source compatibility impact of replacing rethrows
with typed errors will be small.
The ABI between a function with an untyped throws and one that uses typed throws will be different, so that typed throws can benefit from knowing the precise type.
Replacing a rethrows
function with one that uses typed throws, as proposed for the standard library, is an ABI-breaking change. However, it can be done in a manner that doesn't break ABI by retaining the rethrows
function only for binary-compatibility purposes. The existing rethrows
functions will be renamed at the source level (so they don't conflict with the new ones) and made @usableFromInline internal
, which retains the ABI while making the function invisible to clients of the standard library:
@usableFromInline
@_silgen_name(<mangled name of the existing function>)
internal func _oldRethrowingMap<U>(
_ transform: (Wrapped) throws -> U
) rethrows -> U?
Then, the new typed-throws version will be introduced with back-deployment support:
@backDeploy(...)
public func map<U, E>(
_ transform: (Wrapped) throws(E) -> U
) throws(E) -> U?
This way, clients compiled against the updated standard library will always use the typed-throws version. Note that many of these functions are quite small and will be generic, so implementers may opt to use @_alwaysEmitIntoClient
rather than @backDeploy
.
Function declarations must always explicitly specify whether they throw, optionally providing a specific thrown error type. For closures, whether they throw or not is inferred by the Swift compiler. Specifically, the Swift compiler looks at the structure of body of the closure. If the body of the closure contains a throwing site (either a throw
statement or a try
expression) that is not within an exhaustive do...catch
(i.e., one that has an unconditional catch
clause), then the closure is inferred to be throws
. Otherwise, it is non-throwing. Here are some examples:
{ throw E() } // throws
{ try call() } // throws
{
do {
try call()
} catch let e as CatError {
// ...
}
} // throws, the do...catch is not exhaustive
{
do {
try call()
} catch e {}
// ...
}
} // does not throw, the do...catch is exhaustive
With typed throws, the closure type could be inferred to have a typed error by considering all of the throwing sites that aren't caught (let each have a thrown type Ei
) and then inferring the closure's thrown error type to be errorUnion(E1, E2, ... EN)
.
This inference rule will change the thrown error types of existing closures that throw concrete types. For example, the following closure:
{
if Int.random(in: 0..<24) < 20 {
throw CatError.asleep
}
}
will currently be inferred as throws
. With the rule specified here, it will be inferred as throws(CatError)
. This could break some code that depends on the precisely inferred type. To prevent this from becoming a source compatibility problem, we apply the same rule as for do...catch
statements to limit inference: throw
statements within the closure body are treated as having the type any Error
in Swift 5. This way, one can only infer a more specific thrown error type in a closure when the try
operations are calling functions that make use of typed errors.
Note that one can explicitly specify the thrown error type of a closure to disable this type inference, which has the nice effect of also providing a contextual type for throw statements:
{ () throws(CatError) in
if Int.random(in: 0..<24) < 20 {
throw .asleep
}
}
Such a change would need to be under an upcoming feature flag (e.g., FullTypedThrows
) and should also involve inference from the actual thrown error type of throw
statements as well as closing the minor semantic hole introduced for compatibility with rethrows
functions.
The concurrency library has a number of places that could benefit from the adoption of typed throws, including Task
creation and completion, continuations, task cancellation, task groups, and async sequences and streams.
Task
is similar to Result
because it also carries a Failure
type that could benefit from typed throws. Continuations and task groups could propagate typed throws information from closures to make more of the library usable with precise thrown type information.
AsyncSequence
, and the asynchronous for..in
loop that depends on it, could be improved by using typed throws. Both AsyncIteratorProtocol
and AsyncSequence
could be augmented with a Failure
associated type that is used for the thrown error type of next()
, and will be used by the asynchronous for..in
loop to determine whether the sequence can throw. This can be combined with primary associated types to make it possible to use existentials such as any AsyncSequence<Image, NetworkError>
:
public protocol AsyncIteratorProtocol<Element, Failure> {
associatedtype Element
associatedtype Failure: Error = any Error
mutating func next() async throws(Failure) -> Element?
}
public protocol AsyncSequence<Element, Failure> {
associatedtype AsyncIterator: AsyncIteratorProtocol
associatedtype Element where AsyncIterator.Element == Element
associatedtype Failure where AsyncIterator.Failure == Failure
__consuming func makeAsyncIterator() -> AsyncIterator
}
The scope of potential changes to the concurrency library to make full use of typed throws is large. Unlike with the standard library, the adoption of typed throws in the concurrency library requires some interesting design. Therefore, we leave it to a follow-on proposal, noting only that whatever form AsyncSequence
takes with typed throws, the language support for asynchronous for..in
will need to adjust.
The transport mechanism for distributed actors, DistributedActorSystem
, can throw an error due to transport failures. This error is currently untyped, but it should be possible to adopt typed throws (with a Failure
associated type in DistributedActorSystem
and mirrored in DistributedActor
) so that the distributed actor system can be more specific about the kind of error it throws. Calls to a distributed actor from outside the actor (i.e., that could be on a different node) would then throw errorUnion(Failure, E)
where the E
is the type that the function normally throws.
There have been several alternatives to the throws(E)
syntax proposed here. The throws(E)
syntax was chosen because it is syntactically unambiguous, allows arbitrary types for E
, and is consistent with the way in which attributes (like property wrappers or macros with arguments) and modifiers (like unowned(unsafe)
) are written.
The most commonly proposed syntax omits the parentheses, i.e., throws E
. However, this syntax introduces some syntactic ambiguities that would need to be addressed and might cause problems for future evolution of the language:
-
The following code is syntactically ambiguous if
E
is parsed with the arbitrarytype
grammar:func f() throws (E) -> Int { ... }
because the error type would parse as either
(E)
or(E) -> Int
. One could parse a subset of thetype
grammar that doesn't include function types to dodge this ambiguity, with a more complicated grammar. -
The identifier following
throws
could end up conflicting with a future effect:func f() throws E { ... }
If
E
were an effect name in some later Swift version, then there is an ambiguity between typed throws and that effect that we would need to resolve. Future effect modifiers might require more than one argument (and therefore need parentheses), which would make them inconsistent withthrows E
.
Another suggestion uses angle brackets around the thrown type, i.e.,
func f() throws<E> -> Int { ... }
This follows more closely with generic syntax, and highlights the type nature of the arguments more clearly. It's inconsistent with the use of parentheses in modifiers, but has some precedent in attached macros where one can explicitly specify the generic arguments to the macro, e.g., @OptionSet<UInt16>
.
This proposal specifies that a function may throw at most one error type, and if there is any reason to throw more than one error type, one should use any Error
(or the equivalent untyped throws
spelling). It would be possible to support multiple error types, e.g.,
func fetchData() throws(FileSystemError, NetworkError) -> Data
However, this change would introduce a significant amount of complexity in the type system, because everywhere that deals with thrown errors would have to deal with an arbitrary set of thrown errors.
A more reasonable direction to support this use case would be to introduce a form of anonymous enum (often called a sum type) into the language itself, where the type A | B
can be either an A
or B
. With such a feature in place, one could express the function above as:
func fetchData() throws(FileSystemError | NetworkError) -> Data
Trying to introduce multiple thrown error types directly into the language would introduce nearly all of the complexity of sum types, but without the generality, so this proposal only considers a single thrown error type.
This proposal specifies that a function type whose thrown error type is Never
is equivalent to a function type that does not throw. This rule could be generalized from Never
to any uninhabited type, i.e., any type for which we can structurally determine that there is no runtime value. The simplest uninhabited type is a frozen enum with no cases, which is how Never
itself is defined:
@frozen public enum Never {}
However, there are other forms of uninhabited type: a struct
or class
with a stored property of uninhabited type is uninhabited, as is an enum where all cases have an associated value containing an uninhabited type (a generalization of the "no cases" rule mentioned above). This can happen generically. For example, a simple Pair
struct:
struct Pair<First, Second> {
var first: First
var second Second
}
will be uninhabited when either First
or Second
is uninhabited. The Either
enum will be uninhabited when both of its generic arguments are uninhabited. Optional
is never uninhabited, because it's always possible to create a nil
value.
It is possible to generalize the rule about non-throwing function types to consider any function type with an uninhabited thrown error type to be equivalent to a non-throwing function type (all other things remaining equal). However, we do not do so due to implementation concerns: the check for a type being uninhabited is nontrivial, requiring one to walk all of the storage of the type, and (in the presence of indirect enum cases and reference types) is recursive, making it a potentially expensive computation. Crucially, this computation will need to be performed at runtime, to produce proper function type metadata within generic functions:
func f<E: Error>(_: E.Type)) {
typealias Fn = () throws(E) -> Void
let meta = Fn.self
}
f(Never.self) // Fn should be equivalent to () -> Void
f(Either<Never, Never>.self) // Fn should be equivalent to () -> Void
f(Pair<Never, Int>.self) // Fn should be equivalent to () -> Void
The runtime computation of "uninhabited" therefore carries significant cost in terms of the metadata required (one may need to walk all of the storage of the type) as well as the execution time to evaluate that metadata during runtime type formation.
The most plausible route here involves the introduction of an Uninhabited
protocol, which could then be used with conditional conformances to propagate the "uninhabited" type information. For example, Never
would conform to Uninhabited
, and one could conditionally conform a generic error type. For example:
struct WrappedError<E: Error>: Error {
var wrapped: E
}
extension WrappedError: Uninhabited where E: Uninhabited { }
With this, one can express "rethrowing" behavior that wraps the underlying error via typed throws:
func translatesError<E: Error>(f: () throws(E) -> Void) throws(WrappedError<E>) { ... }
Here, when give a non-throwing closure for f
(which infers E = Never
), translatesError
is known not to throw because WrappedError<Never>
is known to be uninhabited (via the conditional conformance). This approach extends to the use of an Either
type to capture errors:
extension Either: Uninhabited when Left: Uninhabited, Right: Uninhabited { }
However, it breaks down when there are two such generic error parameters for something like WrappedError
, because having either one of them be Uninhabited
makes the struct uninhabited, and the generics system does not permit disjunctive constraints like that.
Extending from Never
to arbitrary uninhabited types has some benefits, but requires enough additional design work and complexity that it should constitute a separate proposal. Therefore, we stick with the simpler rule where Never
is the only uninhabited type considered to be special.
A function marked rethrows
throws only when one or more of its closure arguments throws. As note previously, typed throws allows one to more precisely express when the function only rethrows exactly the error from its closure, without translation, as demonstrated with map
:
func map<T, E: Error>(_ transform: (Element) throws(E) -> T) throws(E) -> [T]
However, it cannot express rethrowing behavior when the function is performing translation of errors. For example, consider the following:
func translateErrors<E1: Error, E2: Error>(
f: () throws(E1) -> Void,
g: () throws(E2) -> Void
) ??? {
do {
try f()
} catch {
throw SimpleError(message: "E1: \(error)")
}
do {
try g()
} catch {
throw SimpleError(message: "E2: \(error)")
}
}
This function will only throw when f
or g
throw, and in both cases will translate the errors into SimpleError
. With this proposal, there are two options for specifying the error-handling behavior of translateErrors
, neither of which is precise:
rethrows
correctly communicates that this function throws only when the arguments forf
org
do, but the thrown error type is treated asany Error
.throws(SimpleError)
correctly communicates that this function throws errors of typeSimpleError
, but not that it throws when the argument forf
org
do.
One way to address this would be to allow rethrows
to specify the thrown error type, e.g., rethrows(SimpleError)
, which captures both of the aspects of how this function behaves---when it throws, and what specific error type it throws
.
With typed rethrows
, a bare rethrows
could be treated as syntactic sugar for rethrows(any Error)
, similarly to how throws
is syntactic sugar for throws(any Error)
. This extension is source-compatible and allows one to express more specific error types with throwing behavior.
However, this definition of rethrows
is somewhat unfortunate in a typed-throws world, because it is likely the wrong default. Many use cases for rethrows
do not involve error translation, and would be better served by using typed throws in the manner that map
does. If rethrows
were not already part of the Swift language prior to this proposal, it's likely that we either would not introduce the feature at all, or would treat it as syntactic sugar for typed throws that introduces a generic parameter for the error type that is used for the thrown type of the closure parameters and the function itself. For example:
// rethrows could try rethrows as syntactic sugar..
func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]
// for typed errors:
func map<T, E: Error>(_ transform: (Element) throws(E) -> T) throws(E) -> [T]
Removing or changing the semantics of rethrows
would be a source-incompatible change, so we leave such concerns to a later proposal.
- Revision 6 (post-review):
- Closure type inference did not get implemented in Swift 6.0, so this proposal has been "shrunk" down to what actually got implemented in Swift 6.0.
- Revision 5 (first review):
- Add
do throws(MyError)
{ ... } syntax to allow explicit specification of the thrown error type within the body of ado..catch
block, suppressing type inference of the thrown error type. Thank you to Becca Royal-Gordon for the idea!
- Add
- Revision 4:
- Update the introduction, motivation, and "when to use typed throws" to be more direct.
- Re-incorporate the replacement of
rethrows
functions in the standard library with generic typed throws into the actual proposal. It's so mechanical and straightforward that it doesn't need a separate proposal. - Extend the discussion on API resilience to talk through the source compatibility impacts of replacing a
rethrows
function with one that uses typed throws, since it is quite relevant to this proposal. - Explain that one cannot currently have a thrown error type of
any Error & Codable
or similar because it doesn't conform toError
. - Introduce a compatibility feature to
rethrows
functions to cope with their callees moving to typed throws.
- Revision 3:
- Move the the typed
rethrows
feature out of this proposal, and into Alternatives Considered. Once we gain more experience with typed throws, we can decide what to do withrethrows
. - Expand the discussion on allowing all uninhabited error types to mean "non-throwing".
- Provide a better example for inferring
Error
conformance on generic parameters. - Move the replacement of
rethrows
in the standard library with typed throws into "Future Directions", because it is large enough that it needs a separate proposal. - Move the concurrency library changes for typed throws into "Future Directions", because it is large enough that it needs a separate proposal.
- Add an extended example of replacing the need for
rethrows(unsafe)
with typed throws. - Provide a more significant example of opaque thrown errors that makes use of
Either
internally.
- Move the the typed
- Revision 2:
- Add a short section on when to use typed throws
- Add an Alternatives Considered section for other syntaxes
- Make it clear that only unconditional catches make
do...catch
exhaustive - Update continuation APIs with typed throws
- Add an example of an existential thrown error type
- Describe semantics of
async let
with respect to thrown errors - Add updates to task cancellation APIs