What Is Fatalerror In Swift And When To Use It

What Is Fatalerror In Swift And When To Use It

Even though the signature of the fatalError(_:file:line) function looks a bit daunting, it is surprisingly easy to use. You commonly use the fatalError(_:file:line:) function one of two ways.

Without a message:

fatalError()

With a message:

fatalError("Index Is Invalid")

Easy. Right? But what is the result of calling fatalError(_:file:line:)? If you have some experience with Swift, you have probably already ran into a fatal error or two. The output of the second example looks something like this in Xcode's console.

fatal error: Index Is Invalid: file /Users/Bart/ErrorHandling/ErrorHandling/ViewController.swift, line 20

The message that is passed to the function is printed to the console along with the file and line on which the fatalError(_:file:line:) function is called. And your application immediately stops execution. Did I forget to mention that? This brings us to the next question.

When Should You Throw Fatal Errors?

As the name implies, fatal errors are a type of error. They are typically thrown when the application enters an unknown or unexpected state. As a result, the behavior of the application becomes undefined.

The behavior of the application becomes undefined.

Despite the fact that a fatal error is an error, you cannot catch a fatal error in a do-catch statement. And methods that may throw a fatal error don't need to be marked as throwing.

Because a fatal error terminates your application, some developers argue that it should not be used in production. It can be useful during development, but you should not terminate your application in a production environment. There are good reasons for using and not using fatal errors in Swift.

Downsides

The downside is clear, the application is terminated without any form of warning or precondition validation. I have been very reluctant to use fatal errors up until recently. Let me explain what changed my mind.

Benefits

The benefit of using the fatalError(_:file:line:) function is that you can clearly communicate the occurrence of a programming error. For example, if you attempt to access an element of an array that is out of bounds, a fatal error is thrown.

let array = ["one", "two", "three"]

print(array[3])

This is what you see in the console.

fatal error: Index out of range

In Objective-C, a runtime exception is thrown if you attempt to access an element that doesn't exist. If that exception isn't caught, your application is terminated. The only difference is that fatal errors cannot be caught.

You may be wondering why a fatal error is thrown in the above example. You ask the array for an element that doesn't exist. An array does not return an optional when asked for an element at a particular index, which means throwing a fatal error is the only possibility left. That is the nature of Swift.

This example teaches us two important lessons.

  • Swift's Core Team doesn't shy away from using fatal errors.
  • Fatal errors communicate a clear message to the developer.

The next example shows the use of the fatalError(_:file:line:) function in a scenario that is equally valid in my opinion.

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: IndexPath) -> UITableViewCell {
    if let section = Section(rawValue: indexPath.section) {
        switch section {
        case .info:
            let cell = tableView.dequeueReusableCell(withIdentifier: infoCell, for: indexPath)
            return configureInfoCell(cell)
        case .profile:
            let cell = tableView.dequeueReusableCell(withIdentifier: infoCell, for: indexPath)
            return configureProfileCell(cell)
        case .settings:
            let cell = tableView.dequeueReusableCell(withIdentifier: infoCell, for: indexPath)
            return configureSettingsCell(cell, withModel: settingsModel)
        }
    }

    fatalError("Invalid Section")
}

In the above example, you don't expect the application to ever reach the call to the fatalError(_:file:line:) function. If we omit fatalError("Invalid Section"), the compiler tells us we need to return a valid table view cell.

What is fatalerror in Swift?

While we could omit the if statement, I don't like to force unwrap the initialization of the Section enum. While the result is similar, a fatal error, I prefer to be in control since this is my code.

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: IndexPath) -> UITableViewCell {
    switch Section(rawValue: indexPath.section)! {
    case .info:
        let cell = tableView.dequeueReusableCell(withIdentifier: infoCell, for: indexPath)
        return configureInfoCell(cell)
    case .profile:
        let cell = tableView.dequeueReusableCell(withIdentifier: infoCell, for: indexPath)
        return configureProfileCell(cell)
    case .settings:
        let cell = tableView.dequeueReusableCell(withIdentifier: infoCell, for: indexPath)
        return configureSettingsCell(cell, withModel: settingsModel)
    }
}

As shown in this discussion, another viable option is to create a separate initializer for the enum and throw a fatal error if the enum is initialized with an invalid value.

enum Section: Int {
    case info
    case profile
    case settings

    init(section: Int) {
        guard let value = Section(rawValue: section) else {
            fatalError("Invalid Section (\(section))")
        }

        self = value
    }
}

This is the solution I like best since it simplifies the implementation of tableView(_:cellForRowAtIndexPath:).

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: IndexPath) -> UITableViewCell {
    switch Section(section: indexPath.section) {
    case .info:
        let cell = tableView.dequeueReusableCell(withIdentifier: infoCell, for: indexPath)
        return configureInfoCell(cell)
    case .profile:
        let cell = tableView.dequeueReusableCell(withIdentifier: infoCell, for: indexPath)
        return configureProfileCell(cell)
    case .settings:
        let cell = tableView.dequeueReusableCell(withIdentifier: infoCell, for: indexPath)
        return configureSettingsCell(cell, withModel: settingsModel)
    }
}

What to Remember

Using the fatalError(_:file:line:) function is a personal choice. Some will argue against it, while other will see the advantages and elegance it offers.

If you decide to use fatal errors, use them sparingly. Only use them if the application enters an unknown state, a state it doesn't know how to handle. Don't see fatal errors as an easy way out. Remember that your application will crash and burn if it throws a fatal error.