Assertions allow us to have Swift silently check the state of our program at runtime, but if you want to get them right you need to understand some intricacies. In this article I’ll walk you through the five ways we can make assertions in Swift, and provide clear advice on which to use and when.
Watch the video here, or read the article below
The simplest possible assertion is simply assert()
, which takes a condition to check and a message to print if that check fails. If the conditions fails, your app will crash and the message will be printed. Use it like this:
func sum(of numbers: [Int]) -> Int {
assert(numbers.count > 0, "This should always be given some numbers to sum.")
return numbers.reduce(0, +)
}
Any use of assert()
only applies while you are debugging – it will be compiled out completely when you ship your app to the App Store, as if the whole assert()
line never existed.
As a result, you should assert()
frequently.
“But Paul,” I hear you say, “I already use assert()
.” No. Use it twice as much. Use it five times as much. Seriously, there is no good reason for not adding more assertions to your code: they literally have zero performance impact on your code, and just act as a way of checking that the assumptions you have about your program are valid.
So, add assertions at the start of your functions, checking the values are within the range you expect. Heck, add assertions in the middle of your functions if you want, again making sure everything is how you expect.
Remember, because it gets compiled out completely, don’t be afraid to make your condition slower than you would normally accept, like this:
assert(reallySlowFunction() == true, "Slow function returned an incorrect value")
If you ever find yourself reaching for print()
, consider using assert()
instead – rather than forcing you to read through your output to see what happened, assert()
literally checks the thing you care about and won’t print anything if everything is what you expect.
Importantly, if you build with unchecked optimizations (setting Disable Safety Checks to Yes in your build settings), Swift is free to assume that your assertions are always true. This isn’t so dangerous here, but it can be in some of the other functions below.
TL;DR: Use assert()
regularly; it costs nothing and will help you.
One step up from assert()
is assertionFailure()
, which no longer has a condition: it will always crash your app when the function is called, printing any message you provide. As with assert()
this will only ever be used in debug builds, so you don’t have to worry about this in release mode.
Use it like this:
let number = Int.random(in: 1...3)
switch number {
case 1:
print("You rolled a one!")
case 2:
print("You rolled a two!")
case 3:
print("You rolled a three!")
default:
assertionFailure("Rolling a 3-sided dice shouldn't have any other results.")
}
If you build with unchecked optimizations, the compiler is free to assume that your assertionFailure()
code is never called – it should be unreachable. This is not the same as a failed assertion: using assert()
means “crash when this line is hit in debug mode if my test fails,” but using assertionFailure()
means “this will always crash in debug mode,” so in theory the compiler doesn’t have to worry about any code following the call to assertionFailure()
.
That “in theory” matters: using assertionFailure()
provides an optimization hint, but what Swift’s optimizer does here isn’t specifically defined so you’re in uncertain territory.
TL;DR: Use assertionFailure()
for situations where you know your code won’t be reached, for example if you’ve written a switch
statement and need to put something in default
even though you know it won’t be reached because all other cases are covered.
One step up from assertionFailure()
is precondition()
, which works similarly to assert()
in that you provide it a condition to check and it will crash your code and print a message if the check fails.
Use it like this:
extension Array {
mutating func removeRandom(_ number: Int) {
precondition(count >= number, "Can't remove \(number) items from an array with count \(count)")
for _ in 0..<number {
remove(at: Int.random(in: 0..<count))
}
}
}
Unlike assertions, precondition()
will not be compiled out of release builds, which means the check will take place on your users devices and will crash your code if it fails.
So, you should use preconditions where your checks are designed to preserve data integrity. For example, the Swift standard library uses preconditions to check that you aren’t trying to make a range where the upper bound is less than the lower bound.
If you build with unchecked optimizations, Swift is free to assume that your checks always return true, even in release mode.
TL;DR: Use precondition()
when you need to be absolutely sure the state of your program is what you expect before continuing, even when running on user devices.
Going a step up from precondition()
we have preconditionFailure()
, which is the counterpart to assertionFailure()
: it doesn’t perform a test, but will unconditionally crash your code when reached.
Use it like this:
func updateStatus(to newStatus: String) -> String {
if newStatus == "active" || newStatus == "inactive" {
return "Updating status…"
}
preconditionFailure("Unknown status: \(newStatus)")
}
Like precondition()
, calls to preconditionFailure()
will remain present in your release code, so you should only use this if you’re certain the code should never be reached. However, unlike assert()
, assertionFailure()
, and precondition()
, the preconditionFailure()
function has a return type of Never
, which means the Swift compiler will automatically warn you if you attempt to put executable code after it.
Another important effect of using Never
is that you aren’t forced to return a nonsense value from a function when you know you’re in an invalid state. If we had used assertionFailure()
in the code above, then we’d need to add an extra line returning some sort of string just to make our code compile.
If you build with unchecked optimizations, the compiler is free to assume that your assertionFailure()
code is never called – it should be unreachable.
So, in our code above we’ve made it really clear that updating the user’s status to anything other than “active” and “inactive” is completely invalid – any other kind of change will damage our user’s data, so it’s better to crash rather than continue.
TL;DR: Use preconditionFailure()
when continuing execution is dangerous, and it’s better to halt execution than continue. Swift will explicitly warn you if you put code after a preconditionFailure()
line, because it will never return.
At the very top of the assertion food chain is fatalError()
, which will unconditionally crash your code regardless of you building for debug or release, or even if you have unchecked optimizations enabled – it will always crash your code.
Use it like this:
guard let url = Bundle.main.url(forResource: "input", withExtension: "json") else {
fatalError("Failed to locate input.json in bundle.")
}
Think of fatalError()
as being like preconditionFailure()
, except you cannot turn it off – no level of compiler adjustments will make calls to fatalError()
go away, so you should use them judiciously.
Still, there are at least two reasons why fatalError()
might not be for you:
fatalError()
, !
is more expressive. It's an assertion, not unsafe.”fatalError()
will copy the full path to your current file into your finished program, so you might inadvertently reveal something confidential.TL;DR: Use fatalError()
if you absolutely must crash your code no matter what build settings you use. To quote Ellen Ripley, “nuke it from orbit – it’s the only way to be sure.”
Three of our five options are relatively straightforward:
assert()
as often as you want, because it lets you check things are how you expect without having a performance impact for customers.assertionFailure()
if there’s somewhere your code shouldn’t reach, but it’s not a disaster if it happens.precondition()
anywhere that important checks must happen in order to keep your user safe.That leaves just fatalError()
and preconditionFailure()
, and there’s only one difference between them significant enough for most people: fatalError()
will always cause your program to halt, whereas preconditionFailure()
can be ignored depending on your build settings.
If you’d like to take this tutorial further, here are some suggestions:
assert()
can be used.assert()
– how does @autoclosure
allow our work to be compiled out so cleanly?Here's just a sample of the other tutorials, with each one coming as an article to read and as a 4K Ultra HD video.
Find out more and subscribe here
The Inside Swift series is designed to explore Swift's own source code, so you can better understand how it works and also pick up techniques you can apply to your own code.
Generics are one of the most powerful features of Swift, allowing us to write code once and reuse it in many ways. In this article we’ll explore how they work, why adding constraints actually helps us write more code, and how generics help solve one of the biggest problems in Swift.
In this stream we're going to build a website in Swift, using a free, open-source framework I produced called Ignite. It's designed to be familiar for SwiftUI developers, so hopefully you can see the appeal!
Before you dive in to the first article in this course, I want to give you a brief overview of our goals, how the content is structured, as well as a rough idea of what you can expect to find.
Anyone can write Swift code to fetch network data, but much harder is knowing how to write code to do it respectfully. In this article we’ll look at building a considerate network stack, taking into account the user’s connection, preferences, and more.
Trees are an extraordinarily simple, extraordinarily useful data type, and in this article we’ll make a complete tree data type using Swift in just a few minutes. But rather than just stop there, we’re going to do something quite beautiful that I hope will blow your mind while teaching you something useful.
Phantom types are a powerful way to give the Swift compiler extra information about our code so that it can stop us from making mistakes. In this article I’m going to explain how they work and why you’d want them, as well as providing lots of hands-on examples you can try.
While I’m sure you’re keen to get started programming immediately, please give me a few minutes to outline the goals of this course and explain why it’s different from other courses I’ve written.
Swift's strings are designed to work flawlessly with languages around the world, but sometimes – just sometimes – you need to be careful using them. Let's explore why…
It’s not hard to make a basic property wrapper, but if you want one that automatically updates the body
property like @State
you need to do some extra work. In this article I’ll show you exactly how it’s done, as we build a property wrapper capable of reading and writing documents from our app’s container.
In this article you’ll learn how memoization can dramatically boost the performance of slow functions, and how easy Swift makes it thanks to its generics and closures.
Getting ready for a job interview is tough work, so I’ve prepared a whole bunch of common questions and answers to help give you a jump start. But before you get into them, let me explain the plan in more detail…
In this article we’re going to look at the map()
function, which transforms one thing into another thing. Along the way we’ll also be exploring some core concepts of functional programming, so if you read no other articles in this course at least read this one!
SwiftUI gives us a modifier to make simple shadows, but if you want something more advanced such as inner shadows or glows, you need to do extra work. In this article I’ll show you how to get both those effects and more in a customizable, flexible way.
Swift’s optionals are implemented as simple enums, with just a little compiler magic sprinkled around as syntactic sugar. However, they do much more than people realize, and in this article I’m going to demonstrate some of their power features that can really help you write better code – and blow your mind along the way.
In this article I’m going to walk you through building a WaveView
with SwiftUI, allowing us to create beautiful waveform-like effects to bring your user interface to life.
Reading device motion and orientation is a fast and slightly magical way to incorporate the real world into your apps, and can do a huge amount to add a little spark of delight to your UI. In this article I’m going to show you how easy it is to control SwiftUI layouts using the accelerometer, and give you a few ideas for special effects.
As with so many good interview answers, you should aim to provide a succinct summary first, then expand on your answer with some specifics and examples, and finally get into more nuanced territory based on your real-world experiences.
In this stream we’re going to build a tiny macOS app that stores a notes scratchpad, so the user has instant access to a safe place to store some text. Our first draft will be trivial, but we’ll add important features with a little extra work.
If I could make one question required in all iOS developer job interviews, it would be this one, because it’s a fantastic way to help folks relax, and lets them cherry pick whatever code they think will be most of interest.
Link copied to your pasteboard.