Different ways to catch throwing errors from Swift do-catch

⋅ 9 min read ⋅ Swift

Table of Contents

It is tempting to just catch all a throwing error in a single catch clause and call it a day, but not all errors are created equals, and you should treat it as such.

By learning different ways to catch an error, you can make reasonable catch clauses which easy to read, understand, and match your business needs.

In this article, we will learn different ways to catch errors from throwing functions. Let's briefly introduce a throwing function and know what it uses to separate each catch clause.

Throwing Functions

To indicate that a function, method, or initializer can throw an error, Swift uses the throws keyword in the function’s declaration after its parameters.

func canThrowErrors() throws -> String
func cannotThrowErrors() -> String

But Swift has no way to specify the type of error that throws out. It can be any type that conforms to the Error protocol.

Lacking the way to specify the type means the caller must be responsible for differentiating errors and handling them. That's why it is important to know what tools we have in catching.

Here is an example object which we will use throughout the article.

struct ErrorResponse: Codable { // 1
let code: Int
let message: String
}

enum APIError: Error { // 2
case failedResponse(ErrorResponse)
case unauthorized
case unknown
}

class APIClient { // 3
func makeRequest() throws {
// a throwing function.
}
}

1 A struct that represents an error object.
2 An error type with different cases.
3 A class with methods that can throw an error.

You can easily support sarunw.com by checking out this sponsor.

Sponsor sarunw.com and reach thousands of iOS developers.

Pattern Matching

All catching mechanisms we will talk about today rely on one concept, pattern matching. If the code throws an error in the do clause, it will be matched against the catch clauses using a pattern (if provided) after that catch keyword, and that clause is selected to handle the error.

Her is the general form of a do-catch statement:

do {
    try expression
    // statements
} catch pattern 1 {
    // statements
} catch pattern 2 where condition {
    // statements
} catch pattern 3, pattern 4 where condition {
    // statements
} catch {
    // statements
}

I will guide you through some patterns that I find useful.

Catch all errors

To catch any type of error thrown by the code in the do clause, you provide a catch clause without any pattern.

let client = APIClient()

do {
try client.makeRequest()
} catch { // 1
// 2
print(error)
}

1 A catch without pattern matches any error and binds the error to a local constant, error.
2 We can access error inside the catch clause without explicit declaration of error.

If a catch clause doesn't have a pattern, the clause matches any error and binds the error to a local constant named error. This behavior is why we can reference the error in our print(error) without defining one (2).

Explicit error binding

If you already have a variable named error outside a do-catch statement, the local one will shadow the one outside the catch clause.

let client = APIClient()

// 1
let error = "Outer error"

do {
try client.makeRequest()
} catch {
// 2
print(type(of: error))
// APIError

1 An outer error constant of type string.
2 A local error constant shadows the error string outside the do-catch statement 2. This will print out the type of APIError, not String.

If you want to reference the outer error constant, you have to bind the local error to a new name explicitly. We can do that by using let.

let client = APIClient()

let error = "Outer error"

do {
try client.makeRequest()
} catch let localError { // 1
print(type(of: error))
// String

print(type(of: localError))
// APIError
}

1 We bind the error to a local constant named localError.

By specify let localError next to the catch clause, we tell Swift to bind an error to the localError constant.

Catch a particular type of error

Catch all clause handle any error thrown from the do clause the same way. It is easy, but you can't do much of a handle since it is too generic. The best you can do might be to present users an alert with a generic message like "something went wrong".

So, you likely want to catch an error by its type. Swift provided a variety of patterns for you to use. I have grouped them into four categories.

Catch by error case
Catch by error case with an associated value
Catch by error type
Catch by error type and getting an error value

Catch by error case

You can catch a specific case of error by providing an error case in the pattern.

Here is an example where we want to handle an unauthorized error.

class APIClient {
...
func makeRequestThatNeedUnauthorization() throws {
throw APIError.unauthorized
}
}

let client = APIClient()

do {
try client.makeRequestThatNeedUnauthorization()
} catch APIError.unauthorized { // 1
// Called when error thrown is APIError.unauthorized
// TODO: Present a sign in screen
} catch {
print("others")
}

1 This catch clause will be called if the error thrown is a type of APIError.unauthorized. You might want to handle this specific case by presenting users with a sign-in screen.

Catch by error case with an associated value

Some errors case got associated value. We can access these values by specifying a tuple that contains one element for each associated value.

Here is an example where we try to read ErrorResponse from case failedResponse(ErrorResponse).

class APIClient {
...
func makeFailedRequest() throws {
let response = ErrorResponse(code: 404, message: "File not found")
throw APIError.failedResponse(response)
}
}

let client = APIClient()

do {
try client.makeFailedRequest()
} catch APIError.failedResponse(let errorResponse) { // 1
print(errorResponse)
// ErrorResponse(code: 404, message: "File not found")
} catch {
print("others")
}

1 Specify a tuple with the same number of an element with an associated value. In this case, we have only one associated value. We bind that value to an errorResponse constant.

Where clause

You can also provide a where clause to filter specific conditions of your retrieve value.

Here is an example where we have two catch clauses, one for handling ErrorResponse with 404 and one for other codes.

let client = APIClient()

do {
try client.makeFailedRequest()
} catch APIError.failedResponse(let errorResponse) where errorResponse.code == 404 { // 1
print("Handle only failed response with error code 404")
} catch APIError.failedResponse { // 2
print("Handle other failed response")
} catch {
print("others")
}

1 We add a condition to catch only a ErrorResponse with a code == 404 by put a condition after where clause.
2 Other responses with code other than 404 will fall into the second catch clause.

Catch by error type

So far, we learn how to catch specific cases of an error type. We might not want to handle some errors case by case but as a whole. An example I can think of is DecodingError. Each case of DecodingError tells us a reason why the decode operation failed. This is useful for debugging, but we might not need to handle this case by case since it usually means something wrong from your API, and this information is only goods for debugging.

The DecodingError error cases.

enum DecodingError {
case typeMismatch
case valueNotFound
case keyNotFound
case dataCorrupted
}

In this case, you might want to catch the error by its type. You can do that with a is pattern.

class APIClient {
...
func makeDataRequest() throws -> Data {
return "{\"key\": \"value\"}".data(using: .utf8)!
}
}

let client = APIClient()

do {
let data = try client.makeDataRequest()
let decoder = JSONDecoder()
let user = try decoder.decode(User.self, from: data)
} catch is DecodingError { // 1
print("DecodingError")
} catch is APIError {
print("APIError")
} catch {
print("others")
}

1 We use the is keyword follow by a type of error that we want to match against.

catch is DecodingError will match the DecodingError error regardless of its type.

Catch by error type and getting an error value

Even though we don't want to handle DecodingError by case, we still want to know what kind of error it throws because we want to log it somewhere to fix the problem later. That's means we want to get access to the error value.

To get the error value, we use let and as instead.

struct User: Decodable {
let name: String
}

class APIClient {
...
func makeDataRequest() throws -> Data {
return "{\"key\": \"value\"}".data(using: .utf8)!
}
}

let client = APIClient()

do {
let data = try client.makeDataRequest()
let decoder = JSONDecoder()
let user = try decoder.decode(User.self, from: data)
} catch let error as DecodingError { // 1
print("DecodingError: \(error)")
// DecodingError: keyNotFound(CodingKeys(stringValue: "name", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"name\", intValue: nil) (\"name\").", underlyingError: nil))
} catch is APIError {
print("APIError")
} catch {
print("others")
}

1 let error as DecodingError try to cast an error to DecodingError type, and if successful, bind that to an error local constant.

You can easily support sarunw.com by checking out this sponsor.

Sponsor sarunw.com and reach thousands of iOS developers.

Mix and match

As mentioned earlier, you can have multiple catch clauses with different patterns. So you can mix and match any pattern you have learned into a single do-catch statement. You have seen this in previous examples already, but I want to highlight this again.

Multiple catch clauses

You can have different types of patterns for each catch clause.

do {
let data = try client.makeDataRequest()
let decoder = JSONDecoder()
let user = try decoder.decode(User.self, from: data)
} catch let error as DecodingError {
// Sending error log
} catch APIError.failedResponse(let errorResponse) where errorResponse.code == 404 {
// Handle only failed response with error code 404
} catch APIError.failedResponse {
// Handle other failed response
} catch is APIError {
// Handle the rest of APIError
} catch {
// Other errors
}

Multiple patterns

You can also have multiple patterns within a single catch clause by separate each pattern with a comma (,).

In the following example, we treat APIError.unauthorized and failed in decoding User object (DecodingError) as an unauthorized case and handle them together.

do {
let data = try client.makeDataRequest()
let decoder = JSONDecoder()
let user = try decoder.decode(User.self, from: data)
} catch APIError.unauthorized, is DecodingError { // 1
print("Unauthorized")
} catch {
// Other errors
}

1 We handle multiple patterns together with a comma (,).


Read more article about Swift or see all available topic

Enjoy the read?

If you enjoy this article, you can subscribe to the weekly newsletter.
Every Friday, you'll get a quick recap of all articles and tips posted on this site. No strings attached. Unsubscribe anytime.

Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.

If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.

Become a patron Buy me a coffee Tweet Share
Previous
How to ignore safe area insets in UIKit

Learn how to make UIScrollView, UITableView, and UICollectionView ignore safe area insets and put their content behind that navigation bar.

Next
Pop-Up Buttons in SwiftUI

Learn how to create macOS pop-up buttons in SwiftUI.

← Home