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
}
  1. You tell the compiler that open(_:) throws only BakeryErrors.
  2. You don’t need to specify BakeryError when 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)")
//}
  1. You don’t need to cast error as BakeryError — option-click it to see it’s already of type BakeryError.
  2. You don’t need to specify BakeryError for its cases.
  3. If you don’t delete this catch closure, 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”.

See forum comments
Download course materials from Github
Previous: Introduction Next: Conclusion