Asserts in Swift and Why You Should Be Using Them

Alec O'Connor
Sep 11, 2017 · 7 min read

Assertion: State a fact or belief confidently and forcefully

Have you ever written code and filled it full of print("Something happened here.") nonsense? You know that it should, theoretically, never be called, but on the off chance that it did happen, you’d want to know? Now you have random lines scattered throughout your code that add no real benefit to your app. And what about when — shocker — it does actually happen, but your debugger is so full of spam that you miss it.

My first venture into using asserts in my code was earlier this year while I was cranking out a few test cases at work. At the time I was new to testing, and I started finding all of these assert(value != nil, "Error: ...") tidbits everywhere. I quickly discovered that asserts are an extremely useful testing and debugging tool for your projects.

But what do they do? Asserts require that a given condition is true, or else it will immediately crash the app. But isn’t that bad? In production, yes! You typically want to prevent crashes for the user’s sake, but sometimes the app is in a severe situation where it would be too dangerous to continue. If you are debugging an app, then a crash can be very useful, as it can lead you directly to the problem(s).

When I grab data from a JSON file on the web, I may not always receive what I’m expecting. This could be because of something as simple as a typo, such as attempting to access data["results"] instead of data["result"]. If I’m querying a puppy social media API, for example, I might expect a match for a specific pupper.

But what if that pup’s account was taken down due to too much cuteness? It’s a real situation, and one that people don’t seem to be taking seriously. Anyway, if my code is counting on a certain puppy named “Belle” to exist in the database, and she doesn’t, then my app could respond in strange ways.

let myPuppyInfo: [String: Any] = ...guard let puppyData = myPuppyInfo["puppies"] as? [String: Any],
let Alice = puppyData["Alice"] as? [String: Any],
let Belle = puppyData["Belle"] as? [String: Any] else {
assertionFailure("There was an issue parsing the JSON")
return Result.failure(.unableToParse)
}

In the example above, I expect to get data for two dogs: Alice and Belle. Only, Belle’s account isn’t showing up in the JSON, for some reason? With a quick assert in the JSON logic, I can make sure that this grabs my attention before the public release, which gives me time to make sure that my code properly accounts for the problem.

But why did I include a return after an assert if the program is going to crash anyway? While the return will never be called when you’re debugging, asserts do not get pulled into production code!

If I’m debugging this in Xcode, then the assert will be called and the program will end, but if this code is on the App Store, the assert will be skipped and the app should be able to response appropriately.

In debugging, the assert will cause the program to stop, and it’ll bring your attention to the exact line where the problem arose — which is also where this specific assert is located. The best part is that, while the app has stopped running, the compiler is still open for business. This means that you can still access the variables and their contents to see what the problem is.

Asserts come in three major flavors:

  1. Assertions
  2. Preconditions
  3. Fatal Errors

Each of these are used to stop your program’s execution in a debuggable state, just as if you had set a breakpoint at the beginning of line 256. If a message was included in the assert then it’ll be passed off to the debugger right before stopping.

While asserts, preconditions, and fatal errors produce similar outcomes, their major differences lay in when they are active.

Side-note before we move on: Xcode will compile your build with one of three optimizations flags:

  1. -Onone: Compile without optimizations
  2. -O: Compile with optimization
  3. -Ounchecked: Compile with optimizations and remove runtime safety checks, used for an unchecked release

-Onone is used when you are writing and testing your code. -O, or release, is used for just that, the release version for the App Store. -Ounchecked is a release version that strips the runtime safety checks that Swift thrives off of. I don’t recommend using an unchecked build unless you really know what you’re doing, as it can cause unintended and dangerous results where your app would normally just crash.

Back to asserts!

An assert will only be compiled in Xcode’s Debugging (-Onone) configuration while a precondition will be compiled in both the Debugging (-Onone) and Release (-O) conditions. So what does this mean for you?

Asserts will only be run while you are writing, testing, and debugging code! Preconditions are not nearly as picky and will run there as well as in the final App Store-worthy version.

So when and how do we use them?

Asserts

Asserts should be used when you want to verify your code, but an issue in it wouldn’t necessarily break the app. For example, when you are using an optional initializer and a problem is set to simply return nil. Typically, returning nil is an appropriate way to respond to an error, but it is still technically a band-aid for the actual problem. In Apple’s words:

“Use this function for internal sanity checks that are active during testing but do not impact performance of shipping code.” — Apple Documentation

A great use that I have found for asserts is to drop them in when I would normally print an error that I don’t expect. Guard statements are some of my favorite locations in code to take advantage of an assert.

Swift has two main assert functions: assert(_ condition: Bool, _ message: String) and assertionFailure(_ message: String). That is a dumbed-down version of the actual functions, but for our use, just know that message is an optional parameter that defaults to an empty string.

To use an assert(...) you will need to include a condition to test, such as true or 5 < 10 or var == "String".

Their actual usage can look like this:

let pupName: String? = "Alice"// Continue only if pupName != nil
assert(pupName != nil, "pupName variable is nil")
assert(!pupName.isEmpty)
// ** Success: Continuing execution...
// Continue only if pupName == "Belle"
if (pupName != "Belle") {
assertionFailure("I thought this was Belle?")
}
// ** Failure: End Execution (But only if we are compiled for Debugging)

Fun fact: Swift’s compiler completely ignores asserts when you are compiling for a release version, just like it ignores comments.

Preconditions

Preconditions should be used when you need to meet a necessary condition before continuing with the code, even in production. For example, if Swift detects an out-of-bounds array, it will force the application to crash. In Apple’s words:

“Use this function to detect conditions that must prevent the program from proceeding, even in shipping code.” — Apple Documentation

Preconditions can be used in one of two ways, and their use is almost identical to asserts.

let pupName: String? = "Belle"// Continue only if pupName != nil
precondition(pupName != nil, "pupName variable is nil")
precondition(!pupName.isEmpty)
// ** Success: Continuing execution...
// Continue only if pupName == "Belle"
if (pupName != "Belle") {
preconditionFailure("I thought this was Belle?")
}
// ** Failure: End Execution (If we are compiled for Debugging or Release)

Fatal Errors

The last flavor of assertions that we mentioned above is a fatalError. Fatal errors are essentially preconditionFailures, except for one difference. When a project is compiled for an Unchecked Release (-Ounchecked) like we discussed earlier, it will assume that neither assertionFailure nor preconditionFailure will ever be called.

Using a fatal error is rather simple. Just like a preconditionFailure, the only useful parameter to add is a message, but it is not required. The default value for the message to push to the console will be an empty string.

Some developers may choose to prevent users with a jailbroken phone from using their application to prevent cheating in games or to prevent other security issues. While jailbreaking is much less common these days, there are still older iOS versions that may be vulnerable. If you can detect that a user’s device has been compromised, then a possible solution for your app may be:

let isJailbroken: Bool = ...if (isJailbroken) {
fatalError()
}

Fatal errors are a quick and easy way to quit the execution of the program regardless of the compiler’s build’s optimizations. While checking for a vulnerable device is not as relevant today, fatal errors still have a number of other uses.

A program may also send a fatal error when it is being asked to divide by zero.

For example, imagine that we are working on a banking app. In a last-ditch effort to prevent a data-breach, we want to verify that a user is logged in before showing them sensitive data. If we detect that, for some strange reason, they aren’t, we can make a log of the attempt, send it to the relevant network resource, and then perform a fatal error to hopefully clear whatever bug was present in the app.

Asserts can be extremely useful in Swift when used the right way. What are some ways that you’ve found to use asserts in your projects? You can reach me in the comments below or on Twitter at @_alecoconnor.

Alec O'Connor

Written by

let meMyselfAndI = [“Husband”, “Disney enthusiast”, “Dog Dad”, “iOS Dev”, “Engineer“, “All-things nerd”]

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade