Swift asserts - the missing manual

assertion - a confident and forceful statement of fact or belief. Something declared or stated positively, often with no support or attempt at proof.

The assertions are neat debugging tool. Whenever I need to check my code against condition that is expected to be met, I can use assertion and exception will be thrown or termination occur.

Standard Swift library come with five assertion functions that differ from each other in terms of how they affect flow of the code:

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

I won't discuss here about NSAssert family because they rely on Cocoa Foundation and are not so tight connected to Swift itself.

assert()

Known from C, the assert() is first shot I came along for asserts. As expected, assert() is evaluated only in debug mode (this is the general rule for asserts in programming languages).

for example:

When function transformString() receive empty optional value, appending string to unexpected nil value will terminate execution, as expected, and application will simply crash

func transformString(string: String?) -> String {
    return string! + "_transforme" // expected error if string == nil
}

however I can explicity add check in my code so it will fail in controlled conditions. Using assert() I can check if required condition is met, if not, report issue to developer, right away.

func transformString(string: String?) -> String {
    assert(string != nil, "Invalid parameter") // here
    return string! + "_transforme"
}

Assertion termination indicate that my code encounter a bug, or unhandled situation occur, and I need to investigate why.

This kind of check can be used to mark "programmer errors", this is why it is checked only for debug builds. For release builds, lines with assert() are ommited (condition is not evaluated), as a result behaviour of transformString() is undefined (this one will crash anyway, but in general, it is undefined).

Debug or Release?

At this point it is important to know what option decide if build is debug or release? For Swift it depends on the SWIFT_OPTIMIZATION_LEVEL setting.

SWIFT_OPTIMIZATION_LEVEL

SWIFT_OPTIMIZATION_LEVEL

    SWIFT_OPTIMIZATION_LEVEL = -Onone      // debug
    SWIFT_OPTIMIZATION_LEVEL = -O          // release
    SWIFT_OPTIMIZATION_LEVEL = -Ounchecked // unchecked release

Note: setting DEBUG flags for GCC_PREPROCESSOR_DEFINITIONS, neither ENABLE_NS_ASSERTIONS doesn't matter here.

assertionFailure()

assertionFailure() have similar name to assert(), but it can not be confused. Main difference is that assertionFailure() gives a hint to the optimizer that at this point, given context ("if" branch, or function context) ends, making it the @noreturn effectively. In debug mode application will terminate, but in release behaviour is undefined. This is why compiler warn that code followed by assertionFailure() will never be executed.

func tassertionFailure() {
    assertionFailure("nope")
    println("ever")
}

precondition()

precondition() ensure that the given condition is meet. In case condition is not met, application will terminate. This is more than simplest assert(). Condition is checked for debug and release builds.

func tprecodition() {
    precondition(1 == 2, "not equal")
    println("not equal") // will never be executed
}

For unchecked optimization level (-Ounchecked) optimizer will assume that condition is always met.

preconditionFailure()

preconditionFailure() means fatal error. This one is similar to assertionFailure() and seems the only difference is that preconditionFailure() will emit the trap, while assertionFailure() don't. In this case application will terminate for debug and release builds and for release unchecked (see caution below) build:

func tpreconditionFailure() {
    preconditionFailure("fatal error")
    print("not") // will never be executed
}

Caution: For unchecked builds, the optimizer may assume that this function will never be called. Not very predictable right? so if you decide for unchecked builds, then better go with fatalError()

where is the trap ?

I was not able to catch the SIGTRAP from preconditionFailure(). It suppose to be emited, but I was not able to observe it. I tried to disassemble preconditionFailure() and assertionFailure(), both looks very similar (this is pseudocode by Hopper)

func tpreconditionFailure()

int __TF6result20tpreconditionFailureFT_T_() {
    rax = _TFSSCfMSSFT21_builtinStringLiteralBp8byteSizeBw7isASCIIBi1__SS();
    var_18 = 0x1;
    var_20 = rcx;
    var_28 = "AssertionPlayground.playground/contents.swift";
    var_30 = LODWORD(0x2d);
    var_38 = LODWORD(0x18);
    _TTSf4s_s_s_s___TFSs16_assertionFailedFTVSs12StaticStringSSS_Su_T_("fatal error", LODWORD(0xb), LODWORD(0x2), rax, var_18, var_20, var_28, 0x2d, 0x2, 0x18);
    rax = (*_TFSSCfMSSFT21_builtinStringLiteralBp8byteSizeBw7isASCIIBi1__SS)();
    return rax;
}

func tpreconditionFailure()

int __TF6result17tassertionFailureFT_T_() {
    rax = _TFSSCfMSSFT21_builtinStringLiteralBp8byteSizeBw7isASCIIBi1__SS();
    var_18 = 0x1;
    var_20 = rcx;
    var_28 = "AssertionPlayground.playground/contents.swift";
    var_30 = LODWORD(0x2d);
    var_38 = LODWORD(0x1d);
    _TTSf4s_s_s_s___TFSs16_assertionFailedFTVSs12StaticStringSSS_Su_T_("fatal error", LODWORD(0xb), LODWORD(0x2), rax, var_18, var_20, var_28, 0x2d, 0x2, 0x1d);
    rax = (*_TFSSCfMSSFT21_builtinStringLiteralBp8byteSizeBw7isASCIIBi1__SS)();
    return rax;
}

both seems to call the very same _TTSf4s_s_s_s___TFSs16_assertionFailedFTVSs12StaticStringSSS_Su_T_. Actually I can't say if I'm right or I'm missing something bigger here, all I can say is that I can't catch trap with Xcode 6.3 (6D532l).

CheatSheet

debug release release
function -Onone -O -Ounchecked
assert() YES NO NO
assertionFailure() YES NO NO**
precondition() YES YES NO
preconditionFailure() YES YES YES**
fatalError()* YES YES YES

YES - is for termination, NO - no termination.

* not really assertion, it is designed to terminate code execution always, no matter what.

** the optimizer may assume that this function will never be called.

Conclusion

Assert your code, this is a good practice. Always. With Swift, you have to choose wisely which assertion technique is appropriate unless you want harm yourself. Bear in mind what are consequences of every assertion for debug and release builds. Always do quality assurance to release build before submit to App Store..

Catch me at Twitter: @krzyzanowskim

Updated to Xcode 6.3 (6D554n, beta4)

PS. Cover photo: https://www.flickr.com/photos/kwl/5199093132