Skip to content

Update error handling for typed throws [SE-0413] #296

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 207 additions & 0 deletions TSPL.docc/LanguageGuide/ErrorHandling.md
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,213 @@ let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
```
-->

## Specifying the Error Type

All of the examples above use the most common kind of error handling,
where the errors that your code throws
can be values of any type that conforms to the `Error` protocol.
This approach matches the reality that
you don't know ahead of time every error that could happen
while the code is running,
especially when propagating errors thrown somewhere else.
It also reflects the fact that errors can change over time.
New versions of a library ---
including libraries that your dependencies use ---
can throw new errors,
and the rich complexity of real-world user configurations
can expose failure modes that weren't visible during development or testing.
The error handling code in the examples above
always includes a default case to handle errors
that don't have a specific `catch` clause.

Most Swift code doesn't specify the type for the errors it throws.
However,
you might limit code to throwing errors of only one specific type
in the following special cases:

- When running code on an embedded system
that doesn't support dynamic allocation of memory.
Throwing an instance of `any Error` or another boxed protocol type
requires allocating memory at runtime to store the error.
In contrast,
throwing an error of a specific type
lets Swift avoid heap allocation for errors.

- When the errors are an implementation detail of some unit of code,
like a library,
and aren't part of the interface to that code.
Because the errors come from only the library,
and not from other dependencies or the library's clients,
you can make an exhaustive list of all possible failures.
And because these errors are an implementation detail of the library,
they're always handled within that library.

- In code that only propagates errors described by generic parameters,
like a function that takes a closure argument
and propagates any errors from that closure.
For a comparison between propagating a specific error type
and using `rethrows`,
see <doc:Declarations:Rethrowing-Functions-and-Methods>.

For example,
consider code that summarizes ratings
and uses the following error type:

```swift
enum StatisticsError: Error {
case noRatings
case invalidRating(Int)
}
```

To specify that a function throws only `StatisticsError` values as its errors,
you write `throws(StatisticsError)` instead of only `throws`
when declaring the function.
This syntax is also called *typed throws*
because you write the error type after `throws` in the declaration.
For example,
the function below throws `StatisticsError` values as its errors.

```swift
func summarize(_ ratings: [Int]) throws(StatisticsError) {
guard !ratings.isEmpty else { throw .noRatings }

var counts = [1: 0, 2: 0, 3: 0]
for rating in ratings {
guard rating > 0 && rating <= 3 else { throw .invalidRating(rating) }
counts[rating]! += 1
}

print("*", counts[1]!, "-- **", counts[2]!, "-- ***", counts[3]!)
}
```

In the code above,
the `summarize(_:)` function summarizes a list of ratings
expressed on a scale of 1 to 3.
This function throws an instance of `StatisticsError` if the input isn't valid.
Both places in the code above that throw an error
omit the type of the error
because the function's error type is already defined.
You can use the short form, `throw .noRatings`,
instead of writing `throw StatisticsError.noRatings`
when throwing an error in a function like this.

When you write a specific error type at the start of the function,
Swift checks that you don't throw any other errors.
For example,
if you tried to use `VendingMachineError` from examples earlier in this chapter
in the `summarize(_:)` function above,
that code would produce an error at compile time.

You can call a function that uses typed throws
from within a regular throwing function:

```swift
func someThrowingFunction() -> throws {
let ratings = [1, 2, 3, 2, 2, 1]
try summarize(ratings)
}
```

The code above doesn't specify an error type for `someThrowingFunction()`,
so it throws `any Error`.
You could also write the error type explicitly as `throws(any Error)`;
the code below is equivalent to the code above:

```swift
func someThrowingFunction() -> throws(any Error) {
let ratings = [1, 2, 3, 2, 2, 1]
try summarize(ratings)
}
```

In this code,
`someThrowingFunction()` propagates any errors that `summarize(_:)` throws.
The errors from `summarize(_:)` are always `StatisticsError` values,
which is also a valid error for `someThrowingFunction()` to throw.

Just like you can write a function that never returns
with a return type of `Never`,
you can write a function that never throws with `throws(Never)`:

```swift
func nonThrowingFunction() throws(Never) {
// ...
}
```
This function can't throw because
it's impossible to create a value of type `Never` to throw.

In addition to specifying a function's error type,
you can also write a specific error type for a `do`-`catch` statement.
For example:

```swift
let ratings = []
do throws(StatisticsError) {
try summarize(ratings)
} catch {
switch error {
case .noRatings:
print("No ratings available")
case .invalidRating(let rating):
print("Invalid rating: \(rating)")
}
}
// Prints "No ratings available"
```

In this code,
writing `do throws(StatisticsError)` indicates that
the `do`-`catch` statement throws `StatisticsError` values as its errors.
Like other `do`-`catch` statements,
the `catch` clause can either handle every possible error
or propagate unhandled errors for some surrounding scope to handle.
This code handles all of the errors,
using a `switch` statement with one case for each enumeration value.
Like other `catch` clauses that don't have a pattern,
the clause matches any error
and binds the error to a local constant named `error`.
Because the `do`-`catch` statement throws `StatisticsError` values,
`error` is a value of type `StatisticsError`.

The `catch` clause above uses a `switch` statement
to match and handle each possible error.
If you tried to add a new case to `StatisticsError`
without updating the error-handling code,
Swift would give you an error
because the `switch` statement wouldn't be exhaustive anymore.
For a library that catches all of its own errors,
you could use this approach to ensure any new errors
get corresponding new code to handle them.

If a function or `do` block throws errors of only a single type,
Swift infers that this code is using typed throws.
Using this shorter syntax,
you could write the `do`-`catch` example above as follows:

```swift
let ratings = []
do {
try summarize(ratings)
} catch {
switch error {
case .noRatings:
print("No ratings available")
case .invalidRating(let rating):
print("Invalid rating: \(rating)")
}
}
// Prints "No ratings available"
```

Even though the `do`-`catch` block above
doesn't specify what type of error it throws,
Swift infers that it throws `StatisticsError`.
You can explicitly write `throws(any Error)`
to avoid letting Swift infer typed throws.

## Specifying Cleanup Actions

You use a `defer` statement to execute a set of statements
Expand Down
46 changes: 40 additions & 6 deletions TSPL.docc/ReferenceManual/Declarations.md
Original file line number Diff line number Diff line change
Expand Up @@ -1465,13 +1465,25 @@ func <#function name#>(<#parameters#>) throws -> <#return type#> {
}
```

A function that throws a specific error type has the following form:

```swift
func <#function name#>(<#parameters#>) throws(<#error type#>) -> <#return type#> {
<#statements#>
}
```

Calls to a throwing function or method must be wrapped in a `try` or `try!` expression
(that is, in the scope of a `try` or `try!` operator).

The `throws` keyword is part of a function's type,
and nonthrowing functions are subtypes of throwing functions.
As a result, you can use a nonthrowing function
A function's type includes whether it can throw an error
and what type of error it throws.
This subtype relationship means, for example, you can use a nonthrowing function
in a context where a throwing one is expected.
For more information about the type of a throwing function,
see <doc:Types#Function-Type>.
For examples of working with errors that have explicit types,
see <doc:ErrorHandling#Specifying-a-Concrete-Error-Type>.

You can't overload a function based only on whether the function can throw an error.
That said,
Expand Down Expand Up @@ -1582,6 +1594,28 @@ and a throwing method can't satisfy a protocol requirement for a rethrowing meth
That said, a rethrowing method can override a throwing method,
and a rethrowing method can satisfy a protocol requirement for a throwing method.

An alternative to rethrowing is throwing a specific error type in generic code.
For example:

```swift
func someFunction<E: Error>(callback: () throws(E) -> Void) throws(E) {
try callback()
}
```

This approach to propagating an error
preserves type information about the error.
However, unlike marking a function `rethrows`,
this approach doesn't prevent the function
from throwing an error of the same type.

<!--
TODO: Revisit the comparison between rethrows and throws(E) above,
since it seems likely that the latter will generally replace the former.

See also rdar://128972373
-->

### Asynchronous Functions and Methods

Functions and methods that run asynchronously must be marked with the `async` keyword.
Expand Down Expand Up @@ -1660,7 +1694,7 @@ but the new method must preserve its return type and nonreturning behavior.
> *function-head* → *attributes*_?_ *declaration-modifiers*_?_ **`func`** \
> *function-name* → *identifier* | *operator*
>
> *function-signature* → *parameter-clause* **`async`**_?_ **`throws`**_?_ *function-result*_?_ \
> *function-signature* → *parameter-clause* **`async`**_?_ *throws-clause*_?_ *function-result*_?_ \
> *function-signature* → *parameter-clause* **`async`**_?_ **`rethrows`** *function-result*_?_ \
> *function-result* → **`->`** *attributes*_?_ *type* \
> *function-body* → *code-block*
Expand Down Expand Up @@ -2558,7 +2592,7 @@ See also <doc:Declarations#Initializer-Declaration>.

> Grammar of a protocol initializer declaration:
>
> *protocol-initializer-declaration* → *initializer-head* *generic-parameter-clause*_?_ *parameter-clause* **`throws`**_?_ *generic-where-clause*_?_ \
> *protocol-initializer-declaration* → *initializer-head* *generic-parameter-clause*_?_ *parameter-clause* *throws-clause*_?_ *generic-where-clause*_?_ \
> *protocol-initializer-declaration* → *initializer-head* *generic-parameter-clause*_?_ *parameter-clause* **`rethrows`** *generic-where-clause*_?_

### Protocol Subscript Declaration
Expand Down Expand Up @@ -2903,7 +2937,7 @@ see <doc:Initialization#Failable-Initializers>.

> Grammar of an initializer declaration:
>
> *initializer-declaration* → *initializer-head* *generic-parameter-clause*_?_ *parameter-clause* **`async`**_?_ **`throws`**_?_ *generic-where-clause*_?_ *initializer-body* \
> *initializer-declaration* → *initializer-head* *generic-parameter-clause*_?_ *parameter-clause* **`async`**_?_ *throws-clause*_?_ *generic-where-clause*_?_ *initializer-body* \
> *initializer-declaration* → *initializer-head* *generic-parameter-clause*_?_ *parameter-clause* **`async`**_?_ **`rethrows`** *generic-where-clause*_?_ *initializer-body* \
> *initializer-head* → *attributes*_?_ *declaration-modifiers*_?_ **`init`** \
> *initializer-head* → *attributes*_?_ *declaration-modifiers*_?_ **`init`** **`?`** \
Expand Down
10 changes: 7 additions & 3 deletions TSPL.docc/ReferenceManual/Expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -922,9 +922,13 @@ explicitly marks a closure as throwing or asynchronous.
}
```

If the body of a closure includes a try expression,
If the body of a closure includes a `throws` statement or a `try` expression
that isn't nested inside of a `do` statement with exhaustive error handling,
the closure is understood to be throwing.
Likewise, if it includes an await expression,
If a throwing closure throws errors of only a single type,
the closure is understood as throwing that error type;
otherwise, it's understood as throwing `any Error`.
Likewise, if the body includes an `await` expression,
it's understood to be asynchronous.

There are several special forms
Expand Down Expand Up @@ -1245,7 +1249,7 @@ see <doc:AutomaticReferenceCounting#Resolving-Strong-Reference-Cycles-for-Closur
>
> *closure-expression* → **`{`** *attributes*_?_ *closure-signature*_?_ *statements*_?_ **`}`**
>
> *closure-signature* → *capture-list*_?_ *closure-parameter-clause* **`async`**_?_ **`throws`**_?_ *function-result*_?_ **`in`** \
> *closure-signature* → *capture-list*_?_ *closure-parameter-clause* **`async`**_?_ *throws-clause*_?_ *function-result*_?_ **`in`** \
> *closure-signature* → *capture-list* **`in`**
>
> *closure-parameter-clause* → **`(`** **`)`** | **`(`** *closure-parameter-list* **`)`** | *identifier-list* \
Expand Down
45 changes: 44 additions & 1 deletion TSPL.docc/ReferenceManual/Statements.md
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,9 @@ throw <#expression#>

The value of the *expression* must have a type that conforms to
the `Error` protocol.
If the `do` statement or function that contains the `throw` statement
declares the type of errors it throws,
the value of the *expression* must be an instance of that type.

For an example of how to use a `throw` statement,
see <doc:ErrorHandling#Propagating-Errors-Using-Throwing-Functions>
Expand Down Expand Up @@ -873,6 +876,46 @@ do {
}
```

A `do` statement can optionally specify the type of error it throws,
which has the following form:

```swift
do throws(<#type#>) {
try <#expression#>
} catch <#pattern> {
<#statements#>
} catch {
<#statements#>
}
```

If the `do` statement includes a `throws` clause,
the `do` block can throw errors of only the specified *type*.
The *type* must be
a concrete type that conforms to the `Error` protocol,
an opaque type that conforms to the `Error` protocol,
or the boxed protocol type `any Error`.
If the `do` statement doesn't specify the type of error it throws,
Swift infers the error type as follows:

- If every `throws` statement and `try` expression in the `do` code block
is nested inside of an exhaustive error-handling mechanism,
then Swift infers that the `do` statement is nonthrowing.

- If the `do` code block contains code that throws
errors of only a single type
outside of exhaustive error handling,
other than throwing `Never`,
then Swift infers that the `do` statement throws that concrete error type.

- If the `do` code block contains code that throws
errors of more than a single type
outside of exhaustive error handling,
then Swift infers that the `do` statement throws `any Error`.

For more information about working with errors that have explicit types,
see <doc:ErrorHandling#Specifying-a-Concrete-Error-Type>.

If any statement in the `do` code block throws an error,
program control is transferred
to the first `catch` clause whose pattern matches the error.
Expand Down Expand Up @@ -914,7 +957,7 @@ see <doc:ErrorHandling#Handling-Errors>.

> Grammar of a do statement:
>
> *do-statement* → **`do`** *code-block* *catch-clauses*_?_ \
> *do-statement* → **`do`** *throws-clause*_?_ *code-block* *catch-clauses*_?_ \
> *catch-clauses* → *catch-clause* *catch-clauses*_?_ \
> *catch-clause* → **`catch`** *catch-pattern-list*_?_ *code-block* \
> *catch-pattern-list* → *catch-pattern* | *catch-pattern* **`,`** *catch-pattern-list* \
Expand Down
Loading