Precondition, assertion and fatalError. A short story about how to fail gracefully.

Marcel Mierzejewski
3 min readJun 21, 2020

--

One of the most important principles when developing an app is to code it a way, that doesn’t crash.

I would argue, that it’s not true.

The truth is that crashes are only harmful when they are unexpected. Hopefully, this blog post will help you understand when and how to crash your code.

When should I crash?

Usually, code should be crashed, when an app reaches an unknown state. For example, when the user is not logged into the app, but somehow reaches the profile edit screen. This shouldn’t be possible because of the „Locks and Keys” concept. In that case — when the user somehow opens the lock without a key, it’s perfectly fine to crash the app.

How should I crash?

There are five basic assertions that can be used to check your code against expected condition. Those are:

  1. assert()
  2. assertionFailure()
  3. precondition()
  4. preconditionFailure()
  5. fatalError()

Let’s briefly explain each of them.

assert()

assert takes two arguments, a boolean value, that regulates its behaviour (true for a pass, false for a crash) and a failure message.

assert(condition: Bool, message: String)

Assert is evaluated only in the debug mode. Meaning, that if you run your code inside of the simulator, the assert will trigger. For release builds, assert() will be omitted. Assert’s return type is Void, so the program in debug mode will execute next lines normally. Here’s how to use it:

assert(user.type == .logged, "User has to be logged to access this screen")

assertionFailure()

assertionFailure takes just one argument which is a failure message.

assertionFailure(message: String)

assertionFailure acts very similar to assert. With only onedifference, that assertionFailure assumes, that condition is NOT met. Use this function, when for example, a program enters an unexpected case in switch statement:

switch TableViewSection {
   case .friends:
// Do some stuff
case .search:
// Do other stuff
default:
assertionFailure("User should not be able to reach this point")
}

precondition()

precondition takes two arguments. A boolean value that tells it if to crash and a failure message.

precondition(condition: Bool, message: String)

A precondition works just the sameas assert, withone very important exception — it will crash your code ALWAYS. No matter if it was built for release or debug mode. Use this function sparingly and only when you know something has gone so wrong, that you have to crash the app for the user. Its return type is Void, so the program will evaluate the next line if the condition is true. For example, use it to check if some value is absolutely required to progress further.

precondition(appState != nil, "AppState has to be defined.")

preconditionFailure()

This function takes only one argument, which is a failure message. Its return type is Never which is super important. Here’s more about Never return type.

preconditionFailure(message: String)

Never means, that no line after that function will be triggered. This means, that you should use this one sparingly as well. Some build configurations may be bypassing this function, so it’s usually just better to use fatalError() as your default crashing function.

fatalError()

Last, but not least the most well-known function — fatalError(). It only takes one failure message argument and its return type is also Never.

fatalError(message: String)

Works almost exactly the same as the preconditionFailure() function. It will always crash your code. It’s good practice to use it when you want to load a JSON file in the app bundle. If it crashes, you either forgot to include the file or the data inside is corrupted. Either way, it’s a fundamental logic failure on your behalf, so you should be notified about it as soon as possible.

guard let url = self.url(forResource: file, withExtension: nil) else {
fatalError("Failed to locate \(file) in bundle.")
}

guard let data = try? Data(contentsOf: url) else {
fatalError("Failed to load \(file) from bundle.")
}

Conclusion

Assertions are an awesome debugging tool. Try using them yourself. You’ll be surprised of just how many inconsistencies you can find among conditions. Asserting the state of your app is a very good practice. It allows you to fail quickly hence allowing you to identify bugs quickly and fix them.

Remember to leave a meaningful failure messages!

Hope you find this useful. Have fun implementing assertions into your code.

Until next time :)

--

--