30 March, 20236 minute read

Cool language features: Swift guard statements

There are a lot of programming languages to choose from, and some of them boast incredibly useful features that make life as a software engineer significantly easier. While there's been a lot of convergence in modern programming languages–lambdas and destructuring are everywhere now, for instance–there are still a few gems which you can only experience in one or two different languages.

While there are some articles out there describing cool programming language features, I don't think they go deep enough. I'm going to start documenting interesting and valuable language features on this blog, starting with guard statements in Swift.

What is a guard statement?

All software engineers will be familiar with the two basic branch statements: if and switch1. Swift adds a third one to the mix called guard. A guard statement works similarly to an if as it takes a condition and a subsequent block of code which executes based on the result of the condition.

If a guard statement's condition evaluates to false, then its block of code is executed and that block of code must either terminate execution of the program, or transfer control out of the guard statement's enclosing scope.

Here's an example of the guard statement in action, inside a simple divide function:

Click to copy
enum MathError: Error {  case divisorIsZero} func divide(dividend: Double, divisor: Double) throws -> Double {  guard divisor != 0 else {    throw MathError.divisorIsZero  }   return dividend / divisor}

Our divide function must validate that the provided divisor isn't zero, as division by zero is undefined. In languages other than Swift you would use a plain if statement to perform the validation, but in Swift we can use a guard.

Removing the throw from inside the body of the else clause here results in a compilation error, which provides a compile-time guarantee that invalid values don't propagate through the rest of your function. Most editors will even show you an error here at design-time while you're writing your code.

Throwing an error isn't the only way to satisfy the Swift compiler. The requirement is simply that control gets transferred out of the enclosing block, which in this case is the body of the divide function. Returning a value would also work here–even if doing so isn't mathematically correct.

Breaking out of a loop will also keep the compiler happy, as in the following contrived case:

Click to copy
func getValidDivisors(from array: [Double]) -> [Double] {  var divisors: [Double] = []  for value in array {    guard value != 0 else {      continue    }    divisors.append(value)  }  return divisors}

In the case of getValidDivisors the value of the guard statement becomes a lot clearer once we throw a spanner in the works, such as making our array parameter store Strings. In order to return an array of Doubles, we'll need to parse those strings–and parsing can fail.

The guard statement can rescue us from the "pyramid of doom" in this case. guard statements can be used to initialize new variables–most commonly used to unwrap an optional–and unlike a match statement in Rust or a C++ if initializer, the binding created by a guard is accessible from the containing scope of the guard.

Check out the following example to see this in action:

Click to copy
func getValidDivisors(from array: [String]) -> [Double] {  var divisors: [Double] = []  for value in array {    guard let number = Double(value) else {      continue    }    guard number != 0 else {      continue    }    divisors.append(number)  }  return divisors}

Comparing this to directly translated Rust code2, we need another level of indentation:

Click to copy
fn get_valid_divisors(array: &Vec<String>) -> Vec<f32> {  let mut divisors = Vec::new();  for value in array.iter() {    if let Ok(number) = value.parse() {      if number != 0f32 {        continue;      }      divisors.push(number);    }  }  return divisors;}

These two facets–compiler-enforced termination of control flow and the binding leaking–combine to make a very useful language feature. In Swift codebases it's not uncommon to see the guard statement used more often than the if statement.

Discussion of guard

While the examples above are simple, real-world codebases are often much more complicated. If the logic inside your guard clause is a bit more complicated, it is reasonably easy for someone to come along and edit the code such that execution of the function continues after the validation has failed.

In theory tests will catch a silly mistake like this. In practice you might not have test coverage for this case and even if you do have tests, a design-time error offers a vastly improved developer experience compared to catching the error after a long test suite run.

Its ability to help protect against the pyramid of doom is useful in the context of Swift, but it's not really a killer feature for programming languages in general as there are other methods for minimizing horizontal sprawl. I could probably live without it.

Coming back to the compiler guarantees of guard, it always feels a bit strange to me that the guard isn't something you attach to the signature of a function. In my experience codebases end up with a lot of assertCondition or validateInput functions, and being able to tell the compiler that a false return from these kind of functions is something the programmer should explicitly handle would be pretty valuable. It shouldn't be up to the call site to indicate that a false return is exceptional behavior.

Designing a better guard

So then a "guard function" would work identically to a guard statement, with the one difference being the location of the guard keyword. After calling a "guard function", it is required to handle a false return by moving control flow to somewhere else in the program lest you get a compiler error.

A strawman proposal might look something like this:

Click to copy
function validateDivisor(divisor: number): guard {  return divisor != 0;} function doSomething(input: number): void {  validateDivisor(input); // ❌ compile error  // ...} function doSomethingElse(input: number): void {  if (!validateDivisor()) {    return; // ✅ compiles  }  // ...} function doAnotherThing(input: number): void {  if (validateDivisor(input)) {    // ...  } else {    // ✅ compiles    throw new Error('...');  }}

Indicating a return type of guard turns the validateDivisor function into a guard function, in the similar manner to how specifying a return type of divisor is number would turn our function into a type predicate. Attaching the guard to the function itself means that the compiler is able to enforce proper usage. If validation fails, then control flow must be transferred elsewhere. Sounds simple enough, and ensures that failed validation gets handled upstream.

Actually implementing this in the general form is a nightmare. The Swift guard statement is trivial to implement in a compiler, because there's no need to perform static analysis on the condition. Simply verifying that the body of the else clause terminates the program or transfers control flow elsewhere is sufficient.

"Guard functions," on the other hand, would require the compiler to statically analyze the condition inside the if statement to verify that the exceptional case is handled by the programmer. Complicated conditions involving boolean operators would be hard–if not impossible in some cases–to statically analyze. We can simplify things by requiring that an if statement involving a guard function can't include anything else in the condition–only the guard function call, and a possible ! operator.

The only way to guarantee validation functions are used properly at the moment (in Typescript) is to directly throw inside the validation function, but throwing is a rather disruptive thing to do. Who knows where the nearest try/catch is? And how are you supposed to know that this function throws without digging into its implementation?

An advantage the guard return type has is that simply hovering over the function name and inspecting the signature is enough to know how this function impacts control flow. Even without that–the explicit return makes things extremely obvious to the reader.

I think guard functions would be a really fantastic addition to a variety of languages. While I prefer Rust's Option and Result types for handling errors and validating input, the ergonomics of those types don't come from the types themselves. Rust is built around the existence of Option and Result, and has rich and expressive syntax for interacting with them. There are libraries for languages like Javascript which attempt to port these types over from Rust, but they always feel weird to use due to lack of pattern matching and support in the standard library.

Guard functions on the other hand–or even just plain guard statements–fit in a lot nicer with the semantics of more typical languages. They wouldn't feel out of place at all in any of C#, Go, Java, or Javascript, because this programming language feature is a very incremental addition to the control flow structures which already exist in those languages.

Could we possibly see Swift's guard statement proliferate to other languages? Would it be in the form of the statement, or as part of function signatures? Only time will tell.

  1. Even Python has switch statements now, if you consider its structural pattern matching to be a switch!
  2. This is far from idiomatic Rust. The Vec struct has a rich API, and we can actually write this function as a chain of functions on array.iter(). This is purely intended as a demonstration of guard's binding "leaking" out into the enclosing scope.

Don't want to miss out on new posts?

Join 100+ fellow engineers who subscribe for software insights, technical deep-dives, and valuable advice.

Get in touch 👋

If you're working on an innovative web or AI software product, then I'd love to hear about it. If we both see value in working together, we can move forward. And if not—we both had a nice chat and have a new connection.
Send me an email at hello@sophiabits.com