fatalError vs. preconditionFailure

According to the Standard Library documentation, fatalError and preconditionFailure (or precondition, of course) differ in that fatatError always prints its error message, but preconditionFailure doesn't if it's executed in a release build. I tested it, and this is what happens (Xcode 7.1.1).


This strikes me as very strange. If you choose to use precondition/preconditionFailure, you're indicating that the program's logic has failed. I can't see the value of simply having the app disappear without some message or other in the log. After failure, such a message is the only clue as to what went wrong, and even though it's not normally customer visible, it's possible to ask a customer to look in the log and report any errors from the app.


Am I missing something here? It's not as if error messages in the log would turn it into a frightening place to visit — it's already that, with a horrible mixture of navel-gazing introspection, boring informational messages, and truly scary failure reports, all from OS software components. It's also not as if the log would fill up with app error messages, since there is by definition only one per crash.

Replies

You're missing an efficiency argument, I guess. If you always need an error to be printed then use fatalError.

What's the efficiency argument? We're not talking about assert, which doesn't execute the test in release mode. The only thing that's different (AFAICT) between debug and release for precondition is the printing of the error message.

I think the documentation is pretty clear.

precondition is not evaluated in -Ounchecked builds
(same as Disable Safety Checks in Xcode Build Settings):


fatalError

/// Unconditionally print a `message` and stop execution.
@noreturn public func fatalError(@autoclosure message: () -> String = default, file: StaticString = default, line: UInt = default)


precondition

/// Check a necessary condition for making forward progress.
///
/// Use this function to detect conditions that must prevent the
/// program from proceeding even in shipping code.
///
/// * In playgrounds and -Onone builds (the default for Xcode's Debug
///   configuration): if `condition` evaluates to false, stop program
///   execution in a debuggable state after printing `message`.
///
/// * In -O builds (the default for Xcode's Release configuration):
///   if `condition` evaluates to false, stop program execution.
///
/// * In -Ounchecked builds, `condition` is not evaluated, but the
///   optimizer may assume that it *would* evaluate to `true`. Failure
///   to satisfy that assumption in -Ounchecked builds is a serious
///   programming error.
public func precondition(@autoclosure condition: () -> Bool, @autoclosure _ message: () -> String = default, file: StaticString = default, line: UInt = default)


assert

/// Traditional C-style assert with an optional message.
///
/// Use this function for internal sanity checks that are active
/// during testing but do not impact performance of shipping code.
/// To check for invalid usage in Release builds; see `precondition`.
///
/// * In playgrounds and -Onone builds (the default for Xcode's Debug
///   configuration): if `condition` evaluates to false, stop program
///   execution in a debuggable state after printing `message`.
///
/// * In -O builds (the default for Xcode's Release configuration),
///   `condition` is not evaluated, and there are no effects.
///
/// * In -Ounchecked builds, `condition` is not evaluated, but the
///   optimizer may assume that it *would* evaluate to `true`. Failure
///   to satisfy that assumption in -Ounchecked builds is a serious
///   programming error.
public func assert(@autoclosure condition: () -> Bool, @autoclosure _ message: () -> String = default, file: StaticString = default, line: UInt = default)

The scenario I'm interested in is the default Release configuration (-O), which — as your documentation quote says — stops with no message. So the question still stands: why no message in the Release configuration? What purpose does it serve to suppress the message?

Because the optimizer might be able to do more optimization if it doesn't have to care about the code needed to print the message (and just stop program execution instead)?

So the efficiency argument is that the library function, knowing that it is about the crash the app, is able to crash the app 1 millisecond sooner by skipping over a call to 'print'?


I'm still missing the point of that. AFAICT there's no optimization being done at the call site. It has to be within the standard library's LLVM code, doesn't it? I suppose that if the print calls were optimized away, that would allow the strings passed in the 'file:' and 'line:' parameters to be optimized away too, making the executable smaller. Is that the efficiency argument?

I don't know, the precondition above takes an autoclosure for the message, and it might be advantagous for the optimizer to be able to ignore that. I'm just speculating that the optimizer might be able to generate faster code if it can simplify some piece of code by ignoring all the stuff that would otherwise be related to some precondition message (possible constructing and) printing. Perhaps it makes it easier for it to analyze or inline it, or something like that ... And I guess the call site could be more or less complicated/fast depending on what will be the generated (possibly inlined) code related to the precondition after optimization.


I'm sure someone more knowledgeable than I can give a better answer.


(Might be irrelevant to you particular question but FWIW: Subscript get / set are probably good examples for when it makes sense to use precondition (checking for index out of bounds). That subscript code could be heavily used, so we might need the code for it to be as simple/optimizable/fast as possible during release builds, and to get them even faster we could compile the final product with -Ounchecked, essentially removing the index-checking-code all together (and thus simplifying the subscript get/set code even further).)

Again (and, sorry, I'm not really trying to beat you over the head as the official guardian of Standard Library rationality), in the -O case, the closure is evaluated for precondition (and there's no closure for preconditionFailure anyway). Only the print is skipped. If the app doesn't abort, nothing is faster, in the -O case. (That's what assert/assertionFailure is for.)


I agree it sounds useful to have a library function that has no overhead testing the condition (and doesn't abort either) in the -Ounchecked case, but that's not -O.

(I wrote "autoclosure for the message", and preconditionFailure has its message as an autoclosure too:

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

As I said, I'm only speculating, and I'd like to know the correct answer to your question too.)

Ah, yes, so that would give the optimizer a chance to make the executable smaller (not faster).


I still wish I knew why the decision was taken that printing the message wasn't important in the -O case. Anyway, I'm over it now: I've changed all my preconditions to guard/fatalErrors.

I submitted bug report #23672720, asking for a new 'fatal' global function that has an initial condition parameter, just like 'precondition'. That would give 3 full levels of logging functions, suppressed (more or less) according to optimization level:


1. assert/assertionFailure (-Onone)

2. precondition/preconditionFailure (-Onone and -O)

3. fatal/fatalError (all -O settings)


I think that means nobody has to suffer. 😉

I use precondition failures to indicate developer errors, that should really have been picked up at the development stage. I may provide additional detail in the precondition failure message, that you would want the developer to be able to see, but certainly not for release builds. For example, if a localization is missing, our preconditionFailure message may be "String key '...' is missing from 'xyz' file - make sure to add it here". That is not something we would want to leave in our logs. But if we were to try and access a file from a URL that does not exist, then we may choose to use a fatalError instead for this message (it would be better to handle the failure gracefully in the app, but this is just an example).

One other possibility is privacy.


Suppose that when something fails, you want to log relevant Information. Some of that information might be private to the user (personally identifying information, payment details, or security credentials). Some might be private to the developer (security credentials used by the app to communicate with a backend service, or failure details that implies trade secrets about how the app functions). In either case, you might want copious logging in a debug build and a silent failure in a release build.


This strikes me as just another case of the general distinction between “logs to support development” and “remote logging to support debugging”.