Articles index

Swift fatalError is a fatal error

January 15 2020 by Jeff Johnson
To support this blog please buy my apps StopTheMadness and Underpass

You may have seen the Swift function fatalError() used in code samples, but in my opinion the function ought to be avoided entirely, especially in production code, because using the function exposes TMI in release builds. When fatalError() is called in a source file, the compiler writes the full system path of the source file into the compiled binary, for both debug and release builds, because the function is designed to print the source file path along with its log message. Anyone can see the file path by running strings or otool -tV on the binary from the command line. The output will be something like this:

0000000100001011	leaq	0x34a(%rip), %rdi ## literal pool for: "Fatal error"
0000000100001018	leaq	0x311(%rip), %r9 ## literal pool for: "/Users/Shared/source/FatalError/AppDelegate.swift"

Instead of fatalError() you ought to use preconditionFailure() or assertionFailure() depending on whether you want your app to terminate in release builds. Both of these functions will log the source file path and terminate the app in debug builds, and neither will log the source file path in release builds. The difference is that preconditionFailure() will terminate the app in release builds, while assertionFailure() will not, because assertions are entirely excluded from release builds.

There are situations where you must use preconditionFailure() instead of assertionFailure() in order for your code to compile. For example:

func outlineView(_ outlineView:NSOutlineView, child index:Int, ofItem item:Any?) -> Any {
  if item == nil {
    return root.children[index]
  }
  if let parent = item as? MyKnownType {
    return parent.children[index]
  }
  preconditionFailure()
}

If you try to use assertionFailure() here instead, you get the error "missing return in a function expected to return 'Any'". In debug builds an assertion would prevent the function from falling through to the end, but assertions are excluded from release builds. Thus, you need preconditionFailure() to make the end of the function unreachable in both debug and release builds.

By the way, you can also use the C function abort() in Swift or Objective-C code to unconditionally terminate the app in both debug and release builds without writing the source file path into the binary.

To support this blog please buy my apps StopTheMadness and Underpass

Articles index