assert, precondition and fatalError
Table of Contents
Have you ever crashed your application on purpose? Who would want to do that? What if I told you that there are tools for that and that it can be very useful, do you believe me? Let’s take a look at it together!
assert #
If you come from languages like C or C++, assert will be the keyword you are most familiar with. And for good reason, it does exactly the same thing as an assert in C.
Use this function for internal sanity checks that are active during testing but do not impact performance of shipping code
According to the Apple documentation, assert allows you to test a condition and raise an exception when it is not met. On the other hand a assert instruction is only evaluated in debug.
Here is an example of use:
func IMustReturnTrue() -> Bool {
return false
}
assert(IMustReturnTrue() == false, "Return is invalid")
assertFailure which takes no conditions and will always crash your application :
let rndNumber = Int.random(in: 1...10)
if rndNumber < 1 || rndNumber > 10 {
assertFailure("Invalid number. Must be between 1 and 10")
}
precondition #
Use this function to detect conditions that must prevent the program from proceeding, even in shipping code.
A precondition works exactly the same way as assert, however even in production Swift still executes them and can crash the application. For example:
precondition(users.count > 0, "There must be at least one user.")
It is therefore advisable to use it if you always want to have the crash behavior in production. Be careful then with its usage! Here we also find the little brother preconditionFailure which will also raise an exception. It works the same way as precondition for the build configuration part.
fatalError #
fatalError is a message to stop the execution of the current program. It will display a message (if necessary) with the file and line at which the incident occurs.
fatalError is a Mach exception that will directly stop the execution of the program with the error EXC_BAD_INSTRUCTION. This implies that it is not possible to recover the error with a do…catch. This statement works for all build configurations.
A simple example:
func myFunc() {
let rndNumber = Int.random(in: 1...3)
switch rndNumber {
case 1:
// Do something
case 2:
// Do something else
case 3:
// Do another thing
default:
fatalError("Cannot happen!")
}
}
For more information, please refer to the official documentation from Apple.
Why use them? #
Crashing is better than not knowing what state you are in #
You know as well as I do that to work properly, an application must know its state, must be able to manipulate its data. If at some point the application loses a coherent state, you are out of line and anything can happen: we want to avoid this at all costs.
It can be a source of bugs, create potentially annoying side effects for your users, open security holes and many other mishaps are possible. So rather than staying in these states, it is sometimes better that our application crashes.
What’s more, with all the crash reporting tools we have nowadays, it’s much easier to find out about these errors and fix them for the next release, so is this really a problem? Especially if you have a test team behind you, which will be easier to say: “The application crashed”, than “It was a bit weird there, I don’t really know what happened”.
The case of abstract classes #
If you work with Swift and the object model, you know that Swift does not have an abstract class. It is therefore impossible to force the implementation of one or more methods after inheriting them from another class.
So by default we create our method and leave it empty. The problem here is that nothing will happen if the developer who inherits your class does not override the method. We want him to do it. This is where the previous instructions come in:
class A {
func start() {
fatalError("Must be overrided")
}
}
class B: A {
override func start() {
// Do something
}
}
This way, we make sure that at runtime an error occurs if the developer has not done the necessary in the code. Granted, this is not as good as a real abstract class since in such a case the compiler would have refused to go further, which is impossible here. But at least we avoid side effects as much as possible.
Create an SDK, a library or a framework #
No matter what name you give it, creating a reusable library for you or for any other developer is not easy. Especially if this library allows you to reuse code (creation of subclasses) or has a life cycle (startup, registration of the license, …).
There is often a path to follow to initialize your objects for their proper functioning. But what if the developer doesn’t respect this? Use a simple message in the logs? Display an intrusive pop-up in the UI of his application?
Personally I am not convinced by these methods. For me, the easiest way is to forbid to go further. And for that, there is nothing better than to stop the execution of the program with a fatalError and to validate the provided inputs with a precondition.
The message is clear and easy to understand: something is missing. The developer will then be much more careful about how he implements your library. But stay nice and provide documentation 😉.
How do I test these instructions? #
As we have seen these instructions are very interesting to stop a program or raise an exception in a piece of code that does not have the throw keyword when the conditions are not met.
However as we have also seen, these instructions stop the program and do not propagate the error through the calling stack. It is therefore impossible to catcher these errors with a classic do…try in Swift.
Nevertheless it remains important to test this kind of behavior when doing unit tests. This is extreme behavior that needs to be checked.
But then what happens if such a statement is executed during the unit test run?
The test will be in error. Certainly this is what we want, that the error occurs, but it means that our test will always be displayed with a red cross ❌, while we would like it to be validated ✅.
Let’s take a concrete example:
class MyBaseClass {
/// Call when `start` method is called
/// Determine if class is ready
/// Must be overrided by each subclass
func canStart() -> Bool {
fatalError("Must be overrided")
}
}
Here I want to validate that my MyBaseClass class raises an exception if I call the default canStart method. So I want to do something like this:
import XCTest
final class MyBaseClass: XCTestCase {
func test_canStart_MustBeOverrided() {
let myObject = MyBaseClass()
do {
myObject.canStart()
XCTFail()
} catch {
// Success!
// This is what we want
}
}
}
But this will not work! We have seen it before but fatalError does not raise an exception, Swift but stops the program. This implies that we will not be able to catcher our exception. Remember, fatalError is a Mach exception.
But then how to do?
No choice, we need to get the message issued by fatalError before it stops the application. To make life easier and avoid having to put your hands in Mach, I suggest you use the following package: CwlPreconditionTesting.
As you can see in the documentation, its use is very simple:
import CwlPreconditionTesting
let e = catchBadInstruction {
precondition(false, "THIS PRECONDITION FAILURE IS EXPECTED")
}
Here all expressions causing a EXC_BAD_INSTRUCTION will then be swallowed by and will no longer cause the program to stop.
Applied to our case, this gives the following code:
import XCTest
final class MyBaseClass: XCTestCase {
func test_canStart_MustBeOverrided() {
let myObject = MyBaseClass()
let _ = catchBadInstruction {
let _ = myObject.canStart()
precondition(false, "THIS PRECONDITION FAILURE IS EXPECTED")
}
}
}
And the trick is done! Your test is successful and you are testing the exception correctly. It is important to note, however, that we cannot distinguish between a fatalError and another message producing an EXC_BAD_INSTRUCTION.
Sources #
- https://developer.apple.com/forums/thread/26939
- https://developer.apple.com/documentation/swift/fatalerror(_:file:line:)
- https://developer.apple.com/documentation/swift/assert(_:_:file:line:)
- https://developer.apple.com/documentation/swift/precondition(_:_:file:line:)
- https://cocoacasts.com/what-is-fatalerror-in-swift-and-when-to-use-it
- https://agostini.tech/2017/10/01/assert-precondition-and-fatal-error-in-swift/