Assert, Precondition and Fatal Error in Swift

Sometimes it's better to crash then to have your app running in an inconsistent state. In this short article we'll cover the options you have for crashing and what are the main differences between them.Sometimes it’s better to crash then to have your app running in an inconsistent state. In this short article we’ll cover the options you have for crashing and what are the main differences between them. There’s only five, with some subtle differences. Let’s dive in.

Five Ways to Fail

You have five functions available to you if you want to stop the execution of your app (apart from exit() and abort(), but that’s not the topic of this article). And the five are:

We’ll go over each of them individually. But first…

Swift Optimisation Levels

To understand these assertions you need to understand optimisation levels. When you build your app the compiler will perform optimizations on your code in order to make it run faster. You can have different optimisation levels for different build configurations. These are the optimisation levels we’re interested in:

  • -Onone (default for debug builds)
  • -O (default for release builds)
  • -Ounchecked

You can set the optimisation level from your build settings. I won’t go into details about them here, I’ll just say, don’t use -Ounchecked 🙂 If you have to use it, then you really know what you’re doing.

assert()

assert() is a function that takes in four parameters. Condition and the message are the ones you’ll be using. You probably won’t need file and line number (the last two parameters). With the assert() function you evaluate a condition, and if it evaluates to false, your app will stop executing. The condition will only be evaluated for -Onone builds. In other words, it will only work for debug builds. Let’s see a quick example of using assert:

func printAge(_ age: Int) {
    assert(age >= 0, "Age can't be a negative value")
    
    print("Age is: ", age)
}

printAge(-1) // prints: assertion failed: Age can't be a negative value: file Assertions.playground, line 6

assertionFailure()

If you don’t have a condition to evaluate, or don’t need to evaluate one, you can use assertionFailure() function. It will take a string as an argument to print as the failure message. Like the assert, the function is called only for -Onone builds. Let’s modify our complex example to use assertionFailure 🙂

func printAge(_ age: Int) {
    guard age >= 0 else {
        assertionFailure("Age can't be a negative value")
        return
    }
    print("Age is: ", age)
}

printAge(-1) // prints: fatal error: Age can't be a negative value: file Assertions.playground, line 9

precondition()

precondition() takes the same parameters as assert() and is pretty much doing the same thing. The only difference is that precondition works for -Onone and -O builds. In other words, for default debug and release configurations. You would use it the same way as you would use the assert:

func printAge(_ age: Int) {
    precondition(age >= 0, "Age can't be a negative value")
    
    print("Age is: ", age)
}

printAge(-1) // prints: precondition failed: Age can't be a negative value: file Assertions.playground, line 6

preconditionFailure()

You can see where this is going, right 🙂 preconditionFailure() works the same as assertionFailure(). With the same difference as above, it works for -Onone and -O builds. Again, you would use it the same way you would use the assertionFailure:

func printAge(_ age: Int) {
    guard age >= 0 else {
        preconditionFailure("Age can't be a negative value")
    }
    print("Age is: ", age)
}

printAge(-1) // prints: fatal error: Age can't be a negative value: file Assertions.playground, line 9

If you look a bit closely at the method signature for this function, you’ll see that it has a return type:

public func preconditionFailure(_ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) -> Never

Return type ‘Never’ indicates that this function will never return. It will stop the execution of the app. That’s why Xcode won’t complain about the guard statement falling through because of the missing return statement.

fatalError()

fatalError(), like assertionFailure() and preconditionFailure(), takes a string as an argument that will be printed in the console before the app terminates. It works for all optimisation levels in all build configurations. You use it just like the other two:

func printAge(_ age: Int) {
    guard age >= 0 else {
        fatalError("Age can't be a negative value")
    }
    print("Age is: ", age)
}

printAge(-1) // prints: fatal error: Age can't be a negative value: file Assertions.playground, line 9

And just like the preconditionFailure() it has a return type of ‘Never’.

That brings us to the end of the list.

Conclusion

A discussion at work sparked this article, and I simply wanted to do a bit more research on the subject. We were discussing whether to fail in production or not. Every project is different, and I’m sure you’ll be having heated debates with your colleagues on the same subject. These five methods are the options we have available to fail and it’s up to you to decide which one to use. In my personal opinion, I don’t really see the point of the precondition methods. fatalError makes perfect sense to me, even assert has a use when you’re in the middle of development, or you want to distribute debug builds to your beta customers. Using assertions for this purpose might make sense. Anyway, this is not an article on when to use assertions, I have a feeling it might be a religious topic for some 🙂

I hope you found this article useful, and as always…

Have a nice day 🙂

Dejan.

More resources

4 thoughts on “Assert, Precondition and Fatal Error in Swift

  1. Dejan Agostini Post author

    To be honest with you I never needed to crash in production. And I prefer not to, it’s not a great user experience. But I can imagine a scenario when that’s your only option actually. Imagine you’re running a complex financial app, and in the middle of execution one of your methods returns a completely unexpected result. If you continue to run the app it’s not likely it will fix itself 🙂 So crashing it is ok.

    TLDR; Crashing is OK only in you can’t recover from the state you found yourself in.

    Reply
  2. Ali

    The difference between assert() and assertionFailure() is when you want to input Bool parameter you should use assert() while you can use assertionFailure() for String parameter.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.