Typed Throws
Open the Lesson02-Typed-Throws playground. Start on the Lesson02-typed-throw-a page.
Swift 6.0 introduced typed throws, which allow you to specify the type of Error that a function can throw. Modify open(_:) to throw only BakeryError:
func open(_ shouldOpen: Bool = Bool.random()) throws(BakeryError) -> Bool { // 1
guard shouldOpen else {
throw Bool.random() ? .inventory : .noPower // 2
}
return shouldOpen
}
- You tell the compiler that
open(_:)throws onlyBakeryErrors. - You don’t need to specify
BakeryErrorwhen throwing the error.
The compiler won’t let you throw any other type of Error.
Now, do the same for orderPastry(item:amountRequested:flavor:):
func orderPastry(item: String, amountRequested: Int, flavor: String) throws(BakeryError) -> Int { // 1
guard let pastry = itemsForSale[item] else {
throw .noSuchItem // 2
}
guard flavor == pastry.flavor else {
throw .wrongFlavor
}
guard amountRequested <= pastry.numberOnHand else {
throw .tooFew(numberOnHand: pastry.numberOnHand)
}
pastry.numberOnHand -= amountRequested
return pastry.numberOnHand
}
Again, you specify throws(BakeryError) and delete BakeryError from each BakeryError case.
And, when catching errors thrown by these two methods, remove explicit mentions of BakeryError:
do {
try bakery.open()
try bakery.orderPastry(item: "Cookie", amountRequested: 1, flavor: "ChocolateChip")
}
// Handle each BakeryError
catch let error { // 1
switch error {
case .inventory, .noPower: // 2
print("Sorry, the bakery is now closed.")
case .doNotSell:
print("Sorry, but we don't sell this item.")
case .wrongFlavor:
print("Sorry, but we don't carry this flavor.")
case .tooFew(numberOnHand: let items):
print("We only have \(items) cookies left.")
}
}
//catch { // 3
// print("Something went wrong: \(error)")
//}
- You don’t need to cast
errorasBakeryError— option-click it to see it’s already of typeBakeryError. - You don’t need to specify
BakeryErrorfor its cases. - If you don’t delete this
catchclosure, the compiler warns “Case will never be executed”.
Backwards Compatibility
You can still use throws without specifying an Error type — the compiler translates it to throws(any Error). And, it converts functions that don’t throw an error to throws(Never).
func couldThrow() throws { ... }
// compiler translates this to:
func couldThrow() throws(any Error) { ... }
func neverThrows() { ... }
// compiler translates this to:
func neverThrows() throws(Never) { ... }
Pain Point: At Most One Type
Using typed throws, you can specify at most one type of Error. What if you want your function to throw more than one type of Error?
In the Lesson02-Typed-Throws playground, click Next to open the Lesson02-typed-throw-b page.
In this example, you need to fetch data from a server, and the server requires an authentication token. You define two types of Error:
enum NetworkError: Error {
case unexpected
case disconnected
case timeout(seconds: Int)
case invalidURL(_ url: URL)
case httpError(statusCode: Int)
}
enum AuthError: Error {
case missingToken
case tokenExpired
}
And you write a function loadData() to perform the network request and authentication. If you list both Error types, the compiler flags an error:
func loadData() throws(NetworkError, AuthError) { // compiler error: consecutive statements
// networking code that can throw NetworkError
// authentication code that can throw AuthError
}
What to do? You can fall back to untyped throw:
func loadData() throws {
// networking code can throw NetworkError
// authentication code can throw AuthError
}
And handle thrown errors as before, with two catch ... as ... blocks, plus a third for other possible errors:
do {
try loadData()
}
catch let authError as AuthError {
print("auth error", authError)
switch authError {
case .missingToken:
print("missing token")
// present a login screen
case .tokenExpired:
print("token expired")
// attempt a token refresh
}
}
catch let networkError as NetworkError {
print("network error", networkError)
// present alert explaining what went wrong
}
catch {
print("error", error)
}
Or, you could combine your Error types into a more general Error type:
enum FeedError: Error {
case authError(AuthError)
case networkError(NetworkError)
// include 'other' if you need flexibility
// case other(any Error)
}
Now, you can define a function that throws FeedError and handle thrown errors more compactly. If you don’t include an other case in FeedError, you don’t need to handle any other errors.
func loadData2() throws(FeedError) {
// networking code can throw NetworkError
// authentication code can throw AuthError
}
do {
try loadData2()
}
catch {
switch error {
case .authError(let authError):
// handle auth error
case .networkError(let networkError):
// handle network error
}
// no other error type is possible
}
Of course, if you call another throwing function in the do closure, you’re back to handling any Error in your catch blocks:
func cacheFeed() throws { }
// Two methods might throw different error types so the compiler must drop down to any Error
// since both methods throw something that conforms to Error
do {
try loadData2()
try cacheFeed()
} catch {
// error is any Error here
}
In a real-life application, this approach could lead to an explosion of Error types to wrap other Error types, which could end up in a tangled nest of Error types. And you’d still have to do a lot of switching and checking to handle every error. According to Hacking With Swift, “the authors of the evolution proposal [feel that] even with the addition of typed throws to Swift, untyped throws is better for most scenarios”.