Hallo bananen en druiven, Leo hier. Today we will talk about rethrows keyword in Swift.

I’ve been talking a lot about why error handling is a critical component of any robust application. In Swift, this is achieved through a set of keywords like throw, try, catch, and finally, rethrows. We already addressed the first three ( throw, try, catch ) of them in my previous article so we won’t repeat that today.

While most developers are familiar with the basic error-handling constructs, rethrows often remains underutilized and misunderstood. And it is understandable. First, because error handling per se is not so used in iOS codebases where people would just return Result objects or even an Optional one. The second reason is because rethrows itself is a very niche use case and that is even more rare to happen.

We aim to demystify the rethrows keyword. We’ll explore what it is, how it differs from the standard throws keyword, and why it’s an important tool in your Swift programming arsenal. And one last thing we will dive into inheritance and conformance constraints while using rethrows. By the end of this article, you’ll have a solid understanding of how to effectively use rethrows in your code, enhancing both your application’s reliability and your coding proficiency.

Before we dive into the world of rethrows, past weeks we talked about how can you use protocols to update views in SwiftUI. If that is something that you struggled too, just check this article!

Let’s code! But first…

 

Painting of The Day

The 1602 painting I chose for today is called “Supper at Emmaus” by Caravaggio.

Now in London’s National Gallery, was commissioned by Ciriaco Mattei. It depicts the resurrected Jesus revealing himself to disciples Luke and Cleopas in Emmaus, before vanishing. The painting features life-sized figures against a dark background, with a still-life meal on the table. Caravaggio’s interpretation of Jesus, beardless and blending into ordinary life, contrasts with other representations.

This theme of the sublime merging into the mundane is typical in Caravaggio’s work, emphasizing the intersection of the divine with the everyday. A later, more subdued version was painted in 1606, reflecting Caravaggio’s personal evolution and circumstances.

I chose this art piece today because from one side of Jesus, people are calm and on the other side there’s a man exalted. This shows the duality of rethrows that we will study in this article!

 

The Problem – The Rethrows Keyword in Swift

You were checking some code and found a different keyword named rethrows, what is the meaning of it?

A foundational element of error handling in Swift is the throws keyword, and understanding it is crucial before delving into the nuances of rethrows.  Let’s do a quick recap about throws.

The throws keyword means that a function, method, or closure is capable of throwing an error. This is a key aspect of Swift’s error handling model, which requires errors to be propagated up the call stack. By declaring a function as throws, it communicates to the caller that they must be prepared to handle potential errors, either through try-catch blocks or by further propagating the error.

When we talk about the syntax of declaring a function with throws, it generally follows the pattern func myFunction() throws -> ReturnType. This declaration is applicable to any function where there is a risk of operation failure, such as IO operations or data parsing.

Error propagation is a critical aspect of functions marked with throws. When calling a throws function, the try keyword plays a vital role. Errors from these functions bubble up the call stack, necessitating their handling at some level.

Handling errors thrown by throws functions is typically achieved using do-catch blocks or just using try? syntax when the error doesn’t matter, where errors are caught and handled appropriately. Alternatively, errors can be allowed to propagate to the function’s caller. This approach has its implications and is an important part of understanding error handling in Swift.

 

Introducing the rethrows Keyword

The rethrows keyword in Swift extends the functionality of error handling, building upon the foundation laid by the throws keyword. This section aims to clarify the definition, purpose, and usage of rethrows, distinguishing it from throws, and illustrating its application through practical examples.

rethrows is a unique keyword in Swift that is used in the context of functions that take throwing functions as arguments. The primary purpose of rethrows is to indicate that a function will only throw an error if one of its argument functions throws. This differs from throws, which declares that a function can throw an error regardless of its arguments.

To understand rethrows better, let’s explore its application through code examples. Consider a function executeOperation that takes a closure operation as an argument. If operation can throw an error, using rethrows with logUser allows it to propagate that error:

func logUser(operation: (String) throws -> Void) rethrows {
    try operation()
}

Now the difference for a regular throws function is that the rethrows one doesn’t know before you write the closure if that closure will throw or not. This also changes how the users should call the logUser function, because as you know if a function throws or not that should be handle by the caller of the function. Using rethrows you are saying to the compiler: This function might throw errors but not necessarly and it is up to you ( the compiler ) decide when I have to handle it or not.

Check the example below:

func logUser(operation: () throws -> Void) rethrows {
    try operation()
}

let nonThrowingOperation = { print("just printing stuff, don't need to be handled by the caller") }
logUser(operation: nonThrowingOperation) // this works completely fine

let throwingOperation = { throw NetworkError.unavailable }
logUser(operation: throwingOperation) // ERROR: this doesn't compile and show the error "Call can throw but is not marked with 'try'"

Interesting, so now we have a single function that from the caller perspective depend on what the caller will pass in the parameters. The logUser function doesn’t know if the operation closure will throw or not. If the closure actually throws an Error the compiler force you to handle it otherwise it will not compile.

 

Why rethrows Keyword Exists?

Imagine that you are building the blocks of Swift language, for example the map function. If you take a look into it you will the following signature:

what is the map api in Swift

 

If you look closely the map function accepts a closure that is the transform closure. To give the maximum flexibility for the Swift language users, the language developers added the rethrows feature to enable a more pleasant user experience with the language. If rethrows don’t exist every time you write a map function in Swift you should add try or do-catch block.

Visualize what I’m saying below:

try? [1,2].map {"\($0)"} // if rethrows didn't exist you should use try? or do-catch

// instead you can use just

[1,2].map {"\($0)"} // really nice :D

 

One last bit of learning for today, how rethrows interact with inheritance? Let’s find out!

 

Overriding and Inheritance Using rethrows Keyword

In Swift, the relationship between throwing and rethrowing methods in the context of overriding and protocol conformance is nuanced and important to grasp. A method marked with throws signifies that it can always throw an error, whereas a method marked with rethrows indicates that it might throw an error, but only if its argument functions do.

Crucially, a method that always throws (marked with throws) cannot be used to override a method that might throw (marked with rethrows). This is because doing so would change the fundamental nature of the method from a potentially throwing one to a definitely throwing one. Similarly, a throwing method cannot fulfill a protocol requirement for a rethrowing method.

Conversely, a rethrowing method can indeed override a throwing method. This is possible because a rethrowing method is essentially a ‘might throw’ function, which is a superset of the ‘will throw’ function. This flexibility also applies to protocol conformance, where a rethrowing method can satisfy a protocol requirement for a throwing method.

Check the example below:

 

class BaseClass {
    func showInt(_ operation: () throws -> Int) throws ->  Int {
        try? operation()
        return 2
    }
}

class ChildClass: BaseClass {
    override func showInt(_ operation: () throws -> Int) rethrows -> Int { // OK - Compile and 
        try? operation()
        return 43
    }
}

let child = ChildClass()

child.showInt({ // same rethrows rules apply here, even though the BaseClass is a throwing function.
    //            throw NetworkError.unavailable you need to use try if you throw something here
    return 2
})

Inheritance is easy but not deductible. In my mind I would guess that if a function is throwing the overriding version of it should also throws, but that is not true. You can override one function and add rethrows to it.

However, the opposite is not possible. If your base class rethrows, your conformance can’t only throw.

Check the example below:

 

class BaseClass {
    func showInt(_ operation: () throws -> Int) rethrows ->  Int {
        try? operation()
        return 2
    }
}

class ChildClass: BaseClass { // NOT OK - Don't compile.
    override func showInt(_ operation: () throws -> Int) throws -> Int { // Override of 'rethrows' method should also be 'rethrows'
        try? operation()
        return 43
    }
}

The example above shows what we explained previously. When you have less strict restrictions in your base class or protocol, you CAN NOT override it or conform to it with a more restrictive form. In the example above the ChildClass is trying to override the function with a more restrictive throw clause which is not possible in Swift. 

And that’s it for today!

 

Conclusion – Handling Errors with Rethrows in Swift

As we’ve explored throughout this article, the rethrows keyword in Swift is a nuanced and powerful tool in error handling. It offers flexibility in scenarios where error propagation depends on the behavior of argument functions. This distinct feature of rethrows not only aids in writing clearer and more concise code but also underscores Swift’s commitment to safe and predictable error handling.

To recap, rethrows is essential when creating functions that accept error-throwing closures or functions as arguments. It intelligently allows these functions to propagate errors only when necessary, thereby avoiding the rigidity of always marking functions with throws. This subtlety makes rethrows particularly useful in higher-order functions like map, where error handling is dependent on the provided closure.

We also discussed how rethrows interacts with inheritance and protocol conformance. A rethrows method can override a throws method, but not vice versa. This understanding is crucial when designing class hierarchies and protocols, ensuring that error handling remains consistent and predictable across subclasses and protocol conformances.

In conclusion, understanding and effectively using the rethrows keyword is a testament to a developer’s proficiency in Swift. It not only enhances the reliability of your applications but also showcases your ability to handle errors elegantly. As you continue to develop your Swift programming skills, keep the rethrows keyword in your toolbox for scenarios where flexible and intelligent error handling is required.

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 and help our community to grow.

Thanks for the reading and…

That’s all folks.

Image credit: Featured Painting