Hallo iedereen, Leo hier. Today we will talk about error handling in Swift.

Note that the article today is about “Error Handling” not “Error Throwing” and that distinction is important because Swift is on the verge of having a really good error-throwing feature. While we wait until at least 2024 fall we will study the several types of error catching today.

There’s a huge debate about when to use throws or not. There are several ways to handle bad returns in Swift, namely the three most famous: throwing errors, returning optional, or using Result. Let’s briefly discuss each one of them.

The arguments for throwing errors are vast. Using throws clearly communicates that a function can result in an error. This explicitness improves code readability and intent. throws allows functions to return multiple error types, providing more information about what went wrong. This detail is crucial for debugging and specific error handling. Also encourages the use of do-catch blocks, promoting structured and centralized error handling. This approach is more scalable for complex error-handling scenarios. And finally standardizing how errors are represented and managed across different functions and modules.

The result would be a middle ground between using throws or returning an optional. While it is versatile regarding the use of the actual return type and an Error, it doesn’t guarantee that the users of your APIs will handle the error cases. And maybe that is exactly your intention, if the handling of the error cases is pretty much optional but you still want the capability to do so, the Result type is the way to go.

The last resort of error handling is returning a nil value wrapped in an optional as the return. I don’t know if this can even be considered error handling because there’s no error to be handled here is just the absence of value. Some advantages of returning an optional are: Returning an optional can lead to cleaner code with fewer lines, as it eliminates the need for do-catch blocks. Optionals can be easily unwrapped using optional chaining or coalescing, allowing for more fluent and concise expressions. Lastly, in scenarios where an absence of a value is a common and expected outcome, an optional return type is more semantically appropriate than throwing an error.

If you're a mid/senior iOS developer looking to improve your skills and salary level, join this 100% free online crash course. It's available only until April 28th, so click to get it now!

As I said, there are several ways to handle when things go wrong. We didn’t mention that you can create an object that internally has error properties and avoid all the options above, which is subject to another article more focused on API design.

We already talked about how to handle error cases in SwiftUI. And it is very nice to have simple solutions for new View APIs.

Handling errors is not all that we iOS developers need to take care of, we also need to be smart about how to consume and create our stream of data. In this article, we discussed how to create a protocol that emits values to SwiftUI views within its properties.

Let’s code! But first…

 

Painting Of the Day – The Fall of Phaeton

The artwork that I chose today is called The Fall of Phaeton, it is from 1913 painted by Giorgio de Chirico

Giorgio de Chirico, an Italian painter born in Greece, was a pivotal figure in the Metaphysical art movement and a significant influence on Surrealism. His artistic journey began in Athens and Florence, followed by studies at the Munich Academy of Fine Arts, where he was inspired by Nietzsche and Schopenhauer.

De Chirico’s work, known for its metaphysical questioning of reality, evolved over time. His initial metaphysical period, between 1909 and 1919, featured cityscapes and mannequin-like figures. Later, he advocated a return to traditional techniques and iconography, leading to a classicizing style. Despite changing styles, his early works remained the most acclaimed.

The painting that I chose for today depicts a man falling from his chariot, I hope something can catch that error.

 

The Problem – Handling Errors in Swift

You want to know all the ways that you can handle an error in Swift, so you can take the best approach in your code.

There are several ways to handle errors in Swift. We will start from the most generic one to the most specific one. They are:

  1. Catch any error
  2. Specific catching types of errors
  3. Specific catching types of error cases
  4. Specific catching errors that have associated values
  5. Specific catching errors that have associated values and filtering those associated values
  6. Specific catching errors that you downcast and filter based on error attributes.

 

Before we start to analyze all of the above catching types we need to setup our study environment with the least amount of code possible.

Copy and paste in the playground the code below:

enum NetworkError: Error {
    var message: String {
        switch self {
        case .unavailable:
            "This is not Available"
        case .status(let int):
            "The code is \(int)"
        case .custom:
            "Any custom Stuff"
        }
    }
    
    case unavailable
    case status(Int)
    case custom
}

func fetchUserName() throws -> String {
    throw NetworkError.status(100)
}

Now you are ready to test all of the catching error types in Swift!

 

Catching Any Swift Error

This is the most generic one. Using it is pretty straightforward and you will be able to access any kind of error thrown by the function:

If you're a mid/senior iOS developer looking to improve your skills and salary level, join this 100% free online crash course. It's available only until April 28th, so click to get it now!

do {
    try fetchUserName()
} catch {
    print("Operation doesn't Succeed \(error)")
}

The interesting part of the catch keyword is that we are able to access the error object and we can print or do whatever we need with it inside the catch clause.

 

Specific catching types of errors with Pattern Matching

Going down one step in the ladder of catching error types we can specify the exact type of error that we want to catch. Check below:

do {
    try fetchUserName()
} catch is NetworkError {
    print("is a network error")
} catch {
    print("all other errors")
}

In this case, we need to use the is keyword to specify what type of error will be caught inside that clause.

 

Specific catching types of error cases

The next step to be more specific in error handling is to use not only a certain type of error but also a specific case of that type. Check the code below:

do {
    try fetchUserName()
} catch NetworkError.custom { // only NetworkError of case custom will be handled here
    print("This is a custom Error")
} catch NetworkError.unavailable { // only NetworkError of case unavailable will be handled here
    print("This is an unavailable Error")
} catch {
    print("all other errors")
}

This is very handy when we already know what type of errors the function can throw and we need to respond accordingly.

Another thing that you can do in this category is to add several cases in one catch clause like the example below:

do {
    try fetchUserName()
} catch NetworkError.custom, NetworkError.unavailable { // multiple cases handled in the same catch clause
    print("This is a custom Error")
} catch {
    print("all other errors")
}

 

Specific catching errors that have Associated Values

Error in Swift is an enum, this way it can also have associated values which we can also parse with the catch clause. Check below how you could do it:

do {
    try fetchUserName()
} catch NetworkError.status(let status) {
    print("This error is status \(status)")
} catch {
    print("all other errors")
}

In this example, whenever we get any status error we will print the status code within our catch clause.

 

Specific catching errors that have Associated Values and filtering those associated values

Whenever we get the associated value we can also handle it in the catch clause. You can make assertions there to check if you need to handle specific cases using the where clause. Observe the example below:

 

do {
    try fetchUserName()
} catch NetworkError.status(let status) where status < 50 { 
    print("This error is status code less than 50. Status: \(status) ")
} catch {
    print("all other errors")
}

With this, you can have fine-grained control over what you are catching for

 

Specific catching errors that you Downcast, filter based on error attributes and Error Case.

Finally, the most specific one is where you not only catch the error by the type, but the case and you also check some attributes of it. Check it out:

do {
    try fetchUserName()
} catch let error as NetworkError where error == .unavailable && error.message.count > 5 {
    print("The error message was: \(error.message)")
} catch {
    print("all other errors")
}

// or using a specific case with Associated value
do {
    try fetchUserName()
} catch let error as NetworkError where error == .status(20) && error.message.count > 10 {
    print("The error message was: \(error.message)")
} catch {
    print("all other errors")
}

In the example above we are specifically catching errors with a specific case and with a specific property count.

 

One Catch to Rule Them All

You can mix and match all of the error-catch clauses above mentioned. The only thing that you need to be careful is to always start the catch clauses with the most specific to the most generic, otherwise all errors would fall into the generic one right away. Check the example below:

 

do {
    try fetchUserName()
} catch let error as NetworkError where error == .status(20) && error.message.count > 5 {
    print("The most specific. Not only Checking if is the right case with a very specific value, but also checking if one of the error properties is the one we are looking for. The error message was: \(error.message) ")
} catch NetworkError.status(let status) where status < 50 {
    print("Checking if it is a Network Error of case .status and filtering some status from the result, printing the code = [\(status)]")
} catch NetworkError.status(let status) {
    print("Only checking if it is a Network Error of case .status and print status code = [\(status)]")
} catch is NetworkError {
    print("Just checking if is a Network Error")
} catch {
    print("All other errors, most generic one")
}

It is really cool how many ways we can catch an error in Swift and how flexible and precise you can be in Error handling.

Let’s go to the section of quick Q&A about Errors in Swift.

 

Frequently Asked Questions – Errors in Swift

There was a long time that I didn’t add this section to the article. The idea of this section is to help people doing interviews to have simple but acurate answers about various iOS topics.

Check the most common interview questions below:

  1. How do you define an error in Swift?

    In Swift, errors are represented by values of types that conform to the Error protocol. This can be an enumeration, a class, or a struct.

  2. What are the main keywords used in Swift’s Error Handling?

    The main keywords are throw, do, try, catch, and finally. These are used to throw an error, handle errors by running a block of code, and clean up actions that must be executed whether or not an error occurred.

  3. How does try, try?, and try! differ in Swift?

    try is used before a function that can throw an error. try? converts an error into an optional value, and try! forcefully unwraps the optional result but causes a runtime error if an error is thrown.

  4. Can Swift handle multiple errors at once?

    Yes, you can use multiple catch blocks to handle different errors separately. The catch blocks are executed in the order they are written.

  5. How do you create custom errors in Swift?

    Custom errors can be created by defining types that conform to the Error protocol, usually done using enumerations with associated values for detailed error information.

  6. Is it mandatory to handle every thrown error in Swift?

    Yes, Swift requires all thrown errors to be handled using do-catch blocks, or the error must be propagated to the caller function.

  7. What is the purpose of the rethrows keyword in Swift?

    The rethrows keyword is used to indicate that a function or method only throws an error if one of its function parameters throws an error.

 

And that’s it for today!

 

How to do Error Handling in Swift?

In conclusion, Swift’s approach to error handling is both comprehensive and adaptable, catering to a variety of coding scenarios and preferences.

Whether you prefer the traditional throw and catch mechanism, the more nuanced Result type, or the simplicity of optional returns, Swift offers a solution. It’s crucial to choose the right method based on the context of your application and the specific requirements of your code. The decision should be guided by factors such as the clarity of your error-handling logic, the importance of error specificity, and the overall architectural design of your app.

Remember, effective error handling is not just about catching and handling errors as they occur; it’s also about writing robust, reliable code that minimizes the likelihood of errors in the first place. As Swift continues to evolve, we anticipate more enhancements in error handling, which will further empower developers to write cleaner, safer, and more efficient code. Until then, mastering the current error handling paradigms in Swift is an essential skill for every iOS developer.

Keep experimenting with different methods and choose the one that best aligns with your project’s needs.

Fellow iOS Developers, that’s all. I hope you liked reading this article as much as I enjoyed writing it.

If you want to support this blog you can Buy Me a Coffee or say hello on Twitter. I’m available on LinkedIn or send me an e-mail through the contact page. You can likewise sponsor this blog so I can get my blog free of ad networks.

Thanks for the reading and…

That’s all folks.

Image credit: Featured Painting