Everything You Should Know About Error Handling in Swift 2.0 (but Were Too Afraid to Ask)

Photo of Radosław Szeja

Radosław Szeja

Updated Jul 24, 2023 • 27 min read

The first time I saw error handling on WWDC's Platform State of the Union, I thought "here we go again... exceptions". It quickly became clear that Swift's error handling isn't the same as Objective-C exceptions.

So what does this error handling look like? Where are the differences? Let’s dive into it!

The first time I saw error handling on WWDC's Platform State of the Union, I thought "here we go again... exceptions". It quickly became clear that Swift's error handling isn't the same as Objective-C exceptions. Actually, it introduces several improvements, in both performance and readability. So what does this error handling look like? Where are the differences? Let’s dive into it!

Basics

As you may expect, a few new keywords were introduced. But first of all, let's take a look at this code:

func divide(dividend: Float, by divisor: Float) -> Float {
if divisor == 0.0 {
fatalError("Division by zero is not allowed")
}
return dividend / divisor
}
error1.swift hosted with ❤ by GitHub

A simple division operation, where we check if a divisor is not equal to 0, so we can safely obtain the result. It's not the best implementation in Swift 2.0 and we can improve it with the guard statement.

Guards

Let's modify our function and then explain it:

func divide(dividend: Float, by divisor: Float) -> Float {
guard divisor != 0.0 else {
fatalError("Division by zero is not allowed")
}
return dividend / divisor
}
error2.swift hosted with ❤ by GitHub

What changed here is that guard ensures that the divisor will be different to 0.0, otherwise it immediately prints an error message and stops execution. It also let us know, further in the function implementation, that the divisor will always be correct. This function is pretty simple, if not a bit too simple, but generally guard helps understanding when the function will exit with failure. Is guard different in any way to a simple if statement? Yes, a bit. You can use guard along with let and where to unwrap optionals and check conditions. The significant differences are that unlike if statements, guard unwraps optionals outside of its scope and also that it runs only if conditions are not met, which means it works similarly to assertion (but without crashing an app). Think about guard as a let-else handler rather than an if statement.

ErrorType

Going back to our function - there's still room for improvement. Stopping app execution for an incorrect divisor is not the best solution. We would prefer to somehow handle the problem, rather than just crash the app. There are a few new things in Swift 2.0 that will help us here. The first is ErrorType. It's a protocol that is used in the error handling mechanism. Only errors conforming to it can be thrown. ErrorType also allows bridging to NSError - cool! Let's make our own division error then:

enum DivisionError: ErrorType {
case ByZero
}
error3.swift hosted with ❤ by GitHub

Throw

Great! Now we want to get this error out of our function when an incorrect divisor is given, and we can do this with the throw keyword:

func divide(dividend: Float, by divisor: Float) -> Float {
guard divisor != 0.0 else {
throw DivisionError.ByZero
}
return dividend/divisor
}
error4.swift hosted with ❤ by GitHub

throw lets us literally throw an error out of the function. But wait - the compiler complains here that the error is not handled and it also talks about some throws. We’re already throwing, what's wrong? How can we handle this error? Well it's true that we don't handle this error, but we don't want to right now. We want to pass this responsibility to the user of our convenient function.

Throws

To do this we simply add throws before the return type in the function’s declaration:

func divide(dividend: Float, by divisor: Float) throws -> Float {
guard divisor != 0.0 else {
throw DivisionError.ByZero
}
return dividend / divisor
}
error5.swift hosted with ❤ by GitHub

Now the compiler seems to be much happier. throws marks our function as a throwing one. Actually, our function now has a different type. If we try to assign divide(_:, by:) to the variable of type (Float, Float) -> Float the compiler will protest. The correct type is (Float, Float) throws -> Float. However, you can still assign a non-throwing function/closure to the throwing type, which means that throwing types are supertypes of their non-throwing versions. Confused? Take a look at an example:

func throwingOperation() throws -> Int {}
func nonThrowingOperation() -> Int {}
func performOperation(operation: () -> Int) -> Int {
return operation()
}
performOperation(throwingOperation) // wrong - Invalid conversion from throwing function ... to non-throwing ...
performOperation(nonThrowingOperation) // correct
error6.swift hosted with ❤ by GitHub

If performOperation(_:) would take a throwing type as an argument both calls would be correct.

func performOperation(operation: () throws -> Int) -> Int {
return operation()
}
performOperation(throwingOperation) // correct
performOperation(nonThrowingOperation) // correct
error7.swift hosted with ❤ by GitHub

Try

Now another problem appears - the compiler kindly reminds us that calling operation() "can throw, but it is not marked with try and the error is not handled". Let’s mark it with try then:

func performOperation(operation: () throws -> Int) -> Int {
return try operation()
}
error8.swift hosted with ❤ by GitHub

Did you notice how nicely the compiler informed us that the function we want to call can fail? I think it's great!

Rethrows

We're getting closer, but still we're not handling this error inside the function. I think this is not the best place to handle errors, so what we want to do is to rethrow it along to the caller. Swift 2.0 has another keyword for that - rethrows. This is what it looks like with rethrows:

func performOperation(operation: () throws -> Int) rethrows -> Int {
return try operation()
}
error9.swift hosted with ❤ by GitHub

We used rethrows in the same way as throws, but it's a bit different, because the Swift compiler treats it as a function attribute rather than a type. It behaves a bit like an optional throws, so we can switch smoothly from a throwing to non-throwing function, based on the context this function is used in.

Let’s get back to the previous example and try to call our divide(_: by:) function:

try divide(4, by: 6)
divide.swift hosted with ❤ by GitHub

We know how to call a throwing function, but what about getting the error if something goes wrong?

Do - catch

We will make use of a do-catch statement:

do {
try divide(4, by: 0)
} catch DivisionError.ByZero {
print("Division by zero is not allowed")
}
error10.swift hosted with ❤ by GitHub

It's worth mentioning that do can have any statements you like, including multiple try calls. Also there can be multiple catch statements that will pattern-match different errors, so you don't need to write nested do-catches.

I believe we’ve covered everything about error handling in Swift 2.0. The only thing remaining is the difference between the error handling feature and regular exceptions.

Swift errors are not exceptions

Swift errors are different to Objective-C exceptions for several reasons. The first one is that error handling in Swift is not as brutal as exceptions. When an exception is triggered the stack is unwinded immediately, which may lead to memory leaks. With Swift, there's no such thing, because it just exits the scope.

There's also a performance cost with Objective-C exceptions since they're being resolved at runtime, while the Swift compiler can optimize our code. The entire mechanism also improves readability with syntactic sugar like try and throws/rethrows keywords. This helps developers to work with code and is just easier to read and understand.

Swift also requires you to catch all thrown errors. The situation of an unhandled error will never occur, which is the main difference when comparing to exceptions.

Summary

Error handling is definitely a step forward. We can check preconditions in our function implementation. We can return a meaningful error and handle it gracefully. It's definitely better than Objective-C error handling pattern with passing NSError* to methods, not to mention the exceptions mechanism, which we are being discouraged to use. On top of that, Swift's type system helps us writing all the code.

Well done, Apple!

Photo of Radosław Szeja

More posts by this author

Radosław Szeja

Engineering Lead | Retail at Netguru
Lost with AI?  Get the most important news weekly, straight to your inbox, curated by our CEO  Subscribe to AI'm Informed

Read more on our Blog

Check out the knowledge base collected and distilled by experienced professionals.

We're Netguru

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency.

Let's talk business