Different ways to catch throwing errors from Swift do-catch
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.
AI Grammar: Correct grammar, spell check, check punctuation, and parphrase.
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.
AI Grammar: Correct grammar, spell check, check punctuation, and parphrase.
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 ShareHow 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.