Skip to content

Commit d3bb2ea

Browse files
authored
Update error handling for typed throws [SE-0413] (#296)
Fixes: rdar://118896654
2 parents a5f8584 + 66287ca commit d3bb2ea

File tree

6 files changed

+345
-21
lines changed

6 files changed

+345
-21
lines changed

TSPL.docc/LanguageGuide/ErrorHandling.md

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,213 @@ let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
699699
```
700700
-->
701701

702+
## Specifying the Error Type
703+
704+
All of the examples above use the most common kind of error handling,
705+
where the errors that your code throws
706+
can be values of any type that conforms to the `Error` protocol.
707+
This approach matches the reality that
708+
you don't know ahead of time every error that could happen
709+
while the code is running,
710+
especially when propagating errors thrown somewhere else.
711+
It also reflects the fact that errors can change over time.
712+
New versions of a library ---
713+
including libraries that your dependencies use ---
714+
can throw new errors,
715+
and the rich complexity of real-world user configurations
716+
can expose failure modes that weren't visible during development or testing.
717+
The error handling code in the examples above
718+
always includes a default case to handle errors
719+
that don't have a specific `catch` clause.
720+
721+
Most Swift code doesn't specify the type for the errors it throws.
722+
However,
723+
you might limit code to throwing errors of only one specific type
724+
in the following special cases:
725+
726+
- When running code on an embedded system
727+
that doesn't support dynamic allocation of memory.
728+
Throwing an instance of `any Error` or another boxed protocol type
729+
requires allocating memory at runtime to store the error.
730+
In contrast,
731+
throwing an error of a specific type
732+
lets Swift avoid heap allocation for errors.
733+
734+
- When the errors are an implementation detail of some unit of code,
735+
like a library,
736+
and aren't part of the interface to that code.
737+
Because the errors come from only the library,
738+
and not from other dependencies or the library's clients,
739+
you can make an exhaustive list of all possible failures.
740+
And because these errors are an implementation detail of the library,
741+
they're always handled within that library.
742+
743+
- In code that only propagates errors described by generic parameters,
744+
like a function that takes a closure argument
745+
and propagates any errors from that closure.
746+
For a comparison between propagating a specific error type
747+
and using `rethrows`,
748+
see <doc:Declarations:Rethrowing-Functions-and-Methods>.
749+
750+
For example,
751+
consider code that summarizes ratings
752+
and uses the following error type:
753+
754+
```swift
755+
enum StatisticsError: Error {
756+
case noRatings
757+
case invalidRating(Int)
758+
}
759+
```
760+
761+
To specify that a function throws only `StatisticsError` values as its errors,
762+
you write `throws(StatisticsError)` instead of only `throws`
763+
when declaring the function.
764+
This syntax is also called *typed throws*
765+
because you write the error type after `throws` in the declaration.
766+
For example,
767+
the function below throws `StatisticsError` values as its errors.
768+
769+
```swift
770+
func summarize(_ ratings: [Int]) throws(StatisticsError) {
771+
guard !ratings.isEmpty else { throw .noRatings }
772+
773+
var counts = [1: 0, 2: 0, 3: 0]
774+
for rating in ratings {
775+
guard rating > 0 && rating <= 3 else { throw .invalidRating(rating) }
776+
counts[rating]! += 1
777+
}
778+
779+
print("*", counts[1]!, "-- **", counts[2]!, "-- ***", counts[3]!)
780+
}
781+
```
782+
783+
In the code above,
784+
the `summarize(_:)` function summarizes a list of ratings
785+
expressed on a scale of 1 to 3.
786+
This function throws an instance of `StatisticsError` if the input isn't valid.
787+
Both places in the code above that throw an error
788+
omit the type of the error
789+
because the function's error type is already defined.
790+
You can use the short form, `throw .noRatings`,
791+
instead of writing `throw StatisticsError.noRatings`
792+
when throwing an error in a function like this.
793+
794+
When you write a specific error type at the start of the function,
795+
Swift checks that you don't throw any other errors.
796+
For example,
797+
if you tried to use `VendingMachineError` from examples earlier in this chapter
798+
in the `summarize(_:)` function above,
799+
that code would produce an error at compile time.
800+
801+
You can call a function that uses typed throws
802+
from within a regular throwing function:
803+
804+
```swift
805+
func someThrowingFunction() -> throws {
806+
let ratings = [1, 2, 3, 2, 2, 1]
807+
try summarize(ratings)
808+
}
809+
```
810+
811+
The code above doesn't specify an error type for `someThrowingFunction()`,
812+
so it throws `any Error`.
813+
You could also write the error type explicitly as `throws(any Error)`;
814+
the code below is equivalent to the code above:
815+
816+
```swift
817+
func someThrowingFunction() -> throws(any Error) {
818+
let ratings = [1, 2, 3, 2, 2, 1]
819+
try summarize(ratings)
820+
}
821+
```
822+
823+
In this code,
824+
`someThrowingFunction()` propagates any errors that `summarize(_:)` throws.
825+
The errors from `summarize(_:)` are always `StatisticsError` values,
826+
which is also a valid error for `someThrowingFunction()` to throw.
827+
828+
Just like you can write a function that never returns
829+
with a return type of `Never`,
830+
you can write a function that never throws with `throws(Never)`:
831+
832+
```swift
833+
func nonThrowingFunction() throws(Never) {
834+
// ...
835+
}
836+
```
837+
This function can't throw because
838+
it's impossible to create a value of type `Never` to throw.
839+
840+
In addition to specifying a function's error type,
841+
you can also write a specific error type for a `do`-`catch` statement.
842+
For example:
843+
844+
```swift
845+
let ratings = []
846+
do throws(StatisticsError) {
847+
try summarize(ratings)
848+
} catch {
849+
switch error {
850+
case .noRatings:
851+
print("No ratings available")
852+
case .invalidRating(let rating):
853+
print("Invalid rating: \(rating)")
854+
}
855+
}
856+
// Prints "No ratings available"
857+
```
858+
859+
In this code,
860+
writing `do throws(StatisticsError)` indicates that
861+
the `do`-`catch` statement throws `StatisticsError` values as its errors.
862+
Like other `do`-`catch` statements,
863+
the `catch` clause can either handle every possible error
864+
or propagate unhandled errors for some surrounding scope to handle.
865+
This code handles all of the errors,
866+
using a `switch` statement with one case for each enumeration value.
867+
Like other `catch` clauses that don't have a pattern,
868+
the clause matches any error
869+
and binds the error to a local constant named `error`.
870+
Because the `do`-`catch` statement throws `StatisticsError` values,
871+
`error` is a value of type `StatisticsError`.
872+
873+
The `catch` clause above uses a `switch` statement
874+
to match and handle each possible error.
875+
If you tried to add a new case to `StatisticsError`
876+
without updating the error-handling code,
877+
Swift would give you an error
878+
because the `switch` statement wouldn't be exhaustive anymore.
879+
For a library that catches all of its own errors,
880+
you could use this approach to ensure any new errors
881+
get corresponding new code to handle them.
882+
883+
If a function or `do` block throws errors of only a single type,
884+
Swift infers that this code is using typed throws.
885+
Using this shorter syntax,
886+
you could write the `do`-`catch` example above as follows:
887+
888+
```swift
889+
let ratings = []
890+
do {
891+
try summarize(ratings)
892+
} catch {
893+
switch error {
894+
case .noRatings:
895+
print("No ratings available")
896+
case .invalidRating(let rating):
897+
print("Invalid rating: \(rating)")
898+
}
899+
}
900+
// Prints "No ratings available"
901+
```
902+
903+
Even though the `do`-`catch` block above
904+
doesn't specify what type of error it throws,
905+
Swift infers that it throws `StatisticsError`.
906+
You can explicitly write `throws(any Error)`
907+
to avoid letting Swift infer typed throws.
908+
702909
## Specifying Cleanup Actions
703910

704911
You use a `defer` statement to execute a set of statements

TSPL.docc/ReferenceManual/Declarations.md

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1465,13 +1465,25 @@ func <#function name#>(<#parameters#>) throws -> <#return type#> {
14651465
}
14661466
```
14671467

1468+
A function that throws a specific error type has the following form:
1469+
1470+
```swift
1471+
func <#function name#>(<#parameters#>) throws(<#error type#>) -> <#return type#> {
1472+
<#statements#>
1473+
}
1474+
```
1475+
14681476
Calls to a throwing function or method must be wrapped in a `try` or `try!` expression
14691477
(that is, in the scope of a `try` or `try!` operator).
14701478

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

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

1597+
An alternative to rethrowing is throwing a specific error type in generic code.
1598+
For example:
1599+
1600+
```swift
1601+
func someFunction<E: Error>(callback: () throws(E) -> Void) throws(E) {
1602+
try callback()
1603+
}
1604+
```
1605+
1606+
This approach to propagating an error
1607+
preserves type information about the error.
1608+
However, unlike marking a function `rethrows`,
1609+
this approach doesn't prevent the function
1610+
from throwing an error of the same type.
1611+
1612+
<!--
1613+
TODO: Revisit the comparison between rethrows and throws(E) above,
1614+
since it seems likely that the latter will generally replace the former.
1615+
1616+
See also rdar://128972373
1617+
-->
1618+
15851619
### Asynchronous Functions and Methods
15861620

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

25592593
> Grammar of a protocol initializer declaration:
25602594
>
2561-
> *protocol-initializer-declaration**initializer-head* *generic-parameter-clause*_?_ *parameter-clause* **`throws`**_?_ *generic-where-clause*_?_ \
2595+
> *protocol-initializer-declaration**initializer-head* *generic-parameter-clause*_?_ *parameter-clause* *throws-clause*_?_ *generic-where-clause*_?_ \
25622596
> *protocol-initializer-declaration**initializer-head* *generic-parameter-clause*_?_ *parameter-clause* **`rethrows`** *generic-where-clause*_?_
25632597
25642598
### Protocol Subscript Declaration
@@ -2903,7 +2937,7 @@ see <doc:Initialization#Failable-Initializers>.
29032937

29042938
> Grammar of an initializer declaration:
29052939
>
2906-
> *initializer-declaration**initializer-head* *generic-parameter-clause*_?_ *parameter-clause* **`async`**_?_ **`throws`**_?_ *generic-where-clause*_?_ *initializer-body* \
2940+
> *initializer-declaration**initializer-head* *generic-parameter-clause*_?_ *parameter-clause* **`async`**_?_ *throws-clause*_?_ *generic-where-clause*_?_ *initializer-body* \
29072941
> *initializer-declaration**initializer-head* *generic-parameter-clause*_?_ *parameter-clause* **`async`**_?_ **`rethrows`** *generic-where-clause*_?_ *initializer-body* \
29082942
> *initializer-head**attributes*_?_ *declaration-modifiers*_?_ **`init`** \
29092943
> *initializer-head**attributes*_?_ *declaration-modifiers*_?_ **`init`** **`?`** \

TSPL.docc/ReferenceManual/Expressions.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -922,9 +922,13 @@ explicitly marks a closure as throwing or asynchronous.
922922
}
923923
```
924924

925-
If the body of a closure includes a try expression,
925+
If the body of a closure includes a `throws` statement or a `try` expression
926+
that isn't nested inside of a `do` statement with exhaustive error handling,
926927
the closure is understood to be throwing.
927-
Likewise, if it includes an await expression,
928+
If a throwing closure throws errors of only a single type,
929+
the closure is understood as throwing that error type;
930+
otherwise, it's understood as throwing `any Error`.
931+
Likewise, if the body includes an `await` expression,
928932
it's understood to be asynchronous.
929933

930934
There are several special forms
@@ -1245,7 +1249,7 @@ see <doc:AutomaticReferenceCounting#Resolving-Strong-Reference-Cycles-for-Closur
12451249
>
12461250
> *closure-expression* **`{`** *attributes*_?_ *closure-signature*_?_ *statements*_?_ **`}`**
12471251
>
1248-
> *closure-signature* *capture-list*_?_ *closure-parameter-clause* **`async`**_?_ **`throws`**_?_ *function-result*_?_ **`in`** \
1252+
> *closure-signature* *capture-list*_?_ *closure-parameter-clause* **`async`**_?_ *throws-clause*_?_ *function-result*_?_ **`in`** \
12491253
> *closure-signature* *capture-list* **`in`**
12501254
>
12511255
> *closure-parameter-clause* **`(`** **`)`** | **`(`** *closure-parameter-list* **`)`** | *identifier-list* \

TSPL.docc/ReferenceManual/Statements.md

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,9 @@ throw <#expression#>
729729

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

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

879+
A `do` statement can optionally specify the type of error it throws,
880+
which has the following form:
881+
882+
```swift
883+
do throws(<#type#>) {
884+
try <#expression#>
885+
} catch <#pattern> {
886+
<#statements#>
887+
} catch {
888+
<#statements#>
889+
}
890+
```
891+
892+
If the `do` statement includes a `throws` clause,
893+
the `do` block can throw errors of only the specified *type*.
894+
The *type* must be
895+
a concrete type that conforms to the `Error` protocol,
896+
an opaque type that conforms to the `Error` protocol,
897+
or the boxed protocol type `any Error`.
898+
If the `do` statement doesn't specify the type of error it throws,
899+
Swift infers the error type as follows:
900+
901+
- If every `throws` statement and `try` expression in the `do` code block
902+
is nested inside of an exhaustive error-handling mechanism,
903+
then Swift infers that the `do` statement is nonthrowing.
904+
905+
- If the `do` code block contains code that throws
906+
errors of only a single type
907+
outside of exhaustive error handling,
908+
other than throwing `Never`,
909+
then Swift infers that the `do` statement throws that concrete error type.
910+
911+
- If the `do` code block contains code that throws
912+
errors of more than a single type
913+
outside of exhaustive error handling,
914+
then Swift infers that the `do` statement throws `any Error`.
915+
916+
For more information about working with errors that have explicit types,
917+
see <doc:ErrorHandling#Specifying-a-Concrete-Error-Type>.
918+
876919
If any statement in the `do` code block throws an error,
877920
program control is transferred
878921
to the first `catch` clause whose pattern matches the error.
@@ -914,7 +957,7 @@ see <doc:ErrorHandling#Handling-Errors>.
914957

915958
> Grammar of a do statement:
916959
>
917-
> *do-statement* **`do`** *code-block* *catch-clauses*_?_ \
960+
> *do-statement* → **`do`** *throws-clause*_?_ *code-block* *catch-clauses*_?_ \
918961
> *catch-clauses* → *catch-clause* *catch-clauses*_?_ \
919962
> *catch-clause* → **`catch`** *catch-pattern-list*_?_ *code-block* \
920963
> *catch-pattern-list* → *catch-pattern* | *catch-pattern* **`,`** *catch-pattern-list* \

0 commit comments

Comments
 (0)