Using Guards in Swift to Avoid the Pyramid of Doom

Conditionals are a fundamental part of programming in Swift.

The first conditional statement you learn is the if statement. It’s not the only one, though, nor is it the most used.

Another example is a guard statement. Guard statements are often much more common than if statements.



FREE GUIDE - SwiftUI App Architecture: Design Patterns and Best Practices

MVC, MVVM, and MV in SwiftUI, plus practical lessons on code encapsulation and data flow.

DOWNLOAD THE FREE GUIDE

Table of contents

What is a guard statement, and how is it used in Swift?

In Swift, guard statements are used to transfer control out of a code block if one or more conditions are not met.

As mentioned above, they are conditional statements, just like an if statement. They are often used to check for invalid input or states that are not allowed in a particular part of a program.

Here is an example of a function calculating the area of a rectangle:

func calculateAreaOfRectangle(width: Double, height: Double) -> Double {
	guard width > 0 && height > 0 else {
		return 0
	}
	return width * height
}

The guard statement checks that both the width and height arguments are greater than 0.

  • If either of these conditions is not met, the code block inside the guard statement is executed. This returns 0.
  • If both conditions are met, control is transferred to the following line of code. This calculates and returns the area.

Note that a guard statement must include an else clause. This must transfer control to a code block that exits the current function or loop.

This is necessary because the guard statement is designed to ensure that certain conditions are met. If they are not, the program should not continue.

It is also possible to transfer control by throwing an error, and we will look at how to do that shortly. We’ll also discuss using guard statements to break out of a loop, skip a step or unwrap optionals.

But first, let’s look at when to use a guard statement and its advantages.

When to use a guard statement instead of an if statement

We could rewrite the code above to use an if statement instead of a guard statement.

func calculateAreaOfRectangle(width: Double, height: Double) -> Double {
	if width > 0 && height > 0 {
		return width * height
	}
	return 0
}

So, what is the difference, and why does the guard statement exist?

Guard statements were explicitly designed for early exits. This is their primary purpose.

One key benefit of using a guard statement is that it allows you to handle invalid input or states at the beginning of a function. This prevents any need to check for these conditions throughout the function.

Guard statements can also make your code more readable and easier to maintain, mainly when checking multiple preconditions.

Guard statements also solve the pyramid of doom problem.

What is the pyramid of doom problem, and how do guard statements solve it?

We can best demonstrate the pyramid of doom problem with an example.

Let’s rewrite the function above to print an error whenever the width or height parameters are less than 0.

func calculateAreaOfRectangle(width: Double, height: Double) -> Double? {
	if width > 0 {
		if height > 0 {
			return width * height
		} else {
			print("The height cannot be less than 0")
		}
	} else {
		print("The width cannot be less than 0")
	}
	return nil
}

This is known as the pyramid of doom. Why? Because the indentation level increases as you go deeper into the code. This creates a pyramid-like shape.

This coding style can make it difficult to read and understand the logic of a program. This is especially true if there are many conditions or nested if statements.

If you use a guard statement to test the parameters and exit early, you can rewrite the above code as follows:

func calculateAreaOfRectangle(width: Double, height: Double) -> Double? {
	guard width > 0 else {
		print("The width cannot be less than 0")
		return nil
	}
	guard height > 0 else {
		print("The height cannot be less than 0")
		return nil
	}
	return width * height
}

This code has the same effect as the original, but it is much easier to read and understand. All the conditions are checked at the top level, also known as the happy path, and the code is not nested.

Throwing errors when preconditions are not met

Returning a value is not the only way to transfer control out of a function. Guard statements can also throw errors inside a throwing function.

For example, let’s rewrite the function above to throw an error instead of returning nil when the area of a rectangle cannot be calculated.

enum RectangleError: Error {
	case invalidWidth
	case invalidHeight
}

func calculateAreaOfRectangle(width: Double, height: Double) throws -> Double {
	guard width > 0 else {
		throw RectangleError.invalidWidth
	}
	guard height > 0 else {
		throw RectangleError.invalidHeight
	}
	return width * height
}

Breaking out of a loop or skipping the current step

Guard statements are not only used to transfer control outside of a function. We can also use them inside loops to skip the current step or break out of the loop entirely.

The function below returns all the even numbers in an array until the first negative number.

func extractEvenNumbersUntilFirstNegative(from array: [Int]) -> [Int] {
	var evenNumbers: [Int] = []
	for number in array {
		guard number > 0 else {
			break
		}
		guard number % 2 == 0 else {
			continue
		}
		evenNumbers.append(number)
	}
	return evenNumbers
}

let numbers = [1, 5, 2, 9, 4, 7, 6, -3, 8, 12]
extractEvenNumbersUntilFirstNegative(from: numbers)
// [2, 4, 6]

The function starts by declaring an empty array to store the even numbers. It then iterates over the input array using a for loop. For each number in the collection, it also performs the following steps:

  1. It checks whether the number is greater than 0. If it is not, the break statement causes the loop to exit immediately.
  2. It checks whether the number is even. If it is not, the continue statement causes the loop to skip the current number and move on to the next one.
  3. If the number is both positive and even, it is added to the evenNumbers array.

Unwrapping optionals using a guard let statement

We can also use guard statements to unwrap optionals, by adding a let or var declaration.

For example, we can rewrite the function above to accept an array of strings instead of an array of integers.

func extractEvenNumbers(from array: [String]) -> [Int] {
	var evenNumbers: [Int] = []
	for string in array {
		guard let number = Int(string) else {
			continue
		}
		guard number % 2 == 0 else {
			continue
		}
		evenNumbers.append(number)
	}
	return evenNumbers
}

let strings = ["1", "5", "nope", "4", "again", "6", "not", "8", "12"]
extractEvenNumbers(from: strings)
// [4, 6, 8, 12]

The function converts each string to an integer using the Int initializer.

If the conversion fails, the guard let statement causes the loop to skip over the current string. It might fail if the string is not a valid integer. The loop then moves on to the next one.

Conclusions

While guard statements look very similar to if statements, their purpose is quite different.

While if statements are used for general control flow, guard statements are used to check preconditions and exit a function or a loop as soon as possible.

In practice, the latter happens far more often than the former. So, while I still use if statements from time to time, I almost exclusively use guard statements.

SwiftUI App Architecture: Design Patterns and Best Practices

It's easy to make an app by throwing some code together. But without best practices and robust architecture, you soon end up with unmanageable spaghetti code. In this guide I'll show you how to properly structure SwiftUI apps.

GET THE FREE BOOK NOW

Leave a Comment