How to use Swift fatalError to prevent issues on production.

Tharindu Ramesh Ketipearachchi
9 min readJun 10, 2023

--

You might get confused when you heard the topic that “How to avoid issues using fatalErrors” , since what fatalErrors itself doing is crashing the application (generating an issue). But this is true. fatalError is something we can use effectively in development phase to detect programming errors and fix them before send it to the production. First let’s see what actually the fatalError is.

What is fatalError() ?

fatalError is a function that immediately terminates the execution of the program or a specific code branch and displays an error message. Literary crash the application with the error message. It is commonly used to indicate that the particular section of code should never be executed.

The function syntax comes as this fatalError(_:file:line) we can log the error message, file and line occurred with this as well. We can use this function with message or without message.

fatalError()

fatalError("Xib file is missing")

The output will looks like as follows.

fatal error: Xib file is missing: file /Users/tharindu/MyApp/ViewController.swift, line 20

Why do devs afraid of fatalError ?

Since the fatalError terminates the application, most of the devs are afraid to use it. Because they are afraid that this will trigger at the production and will cause to bad reviews and sometimes million dollar lost for the company due to app crashes. The point is if you correctly use fatalError on your projects, that it should never be triggered on the production. To understand this let’s see what the actual purpose behind introducing fatalError and how we should use it.

How to use fatalError to prevent production issues?

We use fatalError to detect programming errors and mistakes in the development phase. There are programming errors that should never happen. If that so that will leads to huge issues or crashes of the application. It’s important to detect these errors in the development process and fix those before send it to the production. Literary those are must fix errors. There are two ways that developers can detect errors in development.

  1. By Compilation errors
  2. Runtime crashes

When it gives compilation error, you definitely have to fix it. Otherwise you won’t be able to build it. If your app is crashing, you definitely have to fix it as well. You can’t send an app to a production with known crashes. Literally those two ways force developers to fix their errors. If you are developing a critical application that the single issue might cost your organisation a million dollars. You must detect the issues in the development phase and fix those before send it to the production.

Suppose you implement a function on your project that accepting a generic parameter for reusable purpose, but actually you are using that functions to process some integers. In future, there is a high probability that you or a colleague of your team will mistakenly pass double value to that function. Which not gives you any compilation errors, but still cause to some validation error message on the production. So this will always return an an error message on production and never shows the expected calculated value on the screen. This is a bug but you will never be noticed this on development. In here, you should find a way to notify it them before send it to the production. This is the place where fatalError comes to the rescue.

Let’s understand this by an example. Suppose we have two screens in our app, one to display teacher details the other to display student details. Both screen share the same fields like name, phone & address. So we are using same SwiftUI view to display both screens.

But we need to pass teacherID only to teacher screen, because we have to fetch subject, rates and some other details through an API.

import SwiftUI

enum PageType: String, Identifiable {
case teacher, student

var id: String {
self.rawValue
}
}

struct UserDetailsView: View {
let name: String
let phone: String
let address: String
let teacherID: Int?
let pageType: PageType

init(pageType: PageType, name: String, phone: String, address: String, teacherID: Int? = nil) {
self.pageType = pageType
self.name = name
self.phone = phone
self.address = address
self.teacherID = teacherID
}

var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text("Name: \(name)")
Text("Phone: \(phone)")
Text("Address: \(address)")
}
.padding()
.onAppear {
if pageType == .teacher {
if let teacherID = teacherID {
fetchSubjects(teacherID: teacherID)
} else {
showAlert(message: "Can't load subject data teacher ID is missing")
}
}
}
}


func fetchSubjects(teacherID: String) {

}
}

Here, we have added teacherID as an optional parameter. Since we only need that for teacher screen, we have added nil as a default value for it in our init() method. Now you can load the students screen by calling UserDetailsView init() function without providing a teacherID parameter. You can load the teacher screen by call the same init() by providing theteacherID .

//Load student screen 
UserDetailsView(pageType: .student, name: "John Doe", phone: "123-456-7890", address: "123 Main St")
//Load teacher screen
UserDetailsView(pageType: .teacher, name: "Neo John", phone: "990808899", address: "455, AB streed", teacherID: 7788)

If any case teacherID has set to null, I have unwrapped the tacherID optional safely and show an error alert to the users. Now the application is safe from unwrapping crashes as well.

But what if in the future you (or colleague) mistakenly load the teacher screen using the init() method which is not contains teacherID as follows.

//Load teacher screen 
UserDetailsView(pageType: .teacher, name: "Neo John", phone: "990808899", address: "455, AB streed")

Now what’s going to happen is, you load the teacher screen without providing a teacherID , the teacherID unwrapping will trigger the failure case and show showAlert(message: “Can’t load data teacher ID is missing”) error message to the user always. This is a huge issue and user never will be able to see your teacher’s subject details on the screen. The worst thing is you won’t be able to notice this in the development phase and if your QA also missed this, this will be sending to a production without noticing this issue. This is a programming error and as developer it’s your responsibility to catch those issues on development. But how we going to catch those on production. This is where fatalError() is coming to rescue us. let’s see how.

init(pageType: PageType, name: String, phone: String, address: String, teacherID: Int? = nil) {
self.pageType = pageType
self.name = name
self.phone = phone
self.address = address

if pageType == .teacher {
guard let teacherID = teacherID else {
fatalError("teacherID must be provided for teacher screen type")
}
self.teacherID = teacherID
}
}

Here I have added few extra lines to our init() method. What I have done here is, basically added a check for pageType teacher and unwrapped our teacherID using guard let statement. If it’s null, we are returning a fatalError with a message. Now what’s happening is as soon as developer run this application and load the teacher screen, it will crash with a proper error message. Then the developer must have to fix this issue. This will guarantee that error will be detected at the development time and get it fixed eventually. That’s why fatalError is really useful when detecting programming errors at the development time. Also we can assure that it will never trigger at the production because we’ll definitely catch and fix it in the development. So you never ever should be worried about that.

Where we should use it?

There are certain situations that the fatalError being recommended to be using.

  1. Programming Errors
  2. Preconditions
  3. Security Issues
  4. Unrecoverable Issues

Logical errors or inconsistencies in your code that should never occur during normal program execution are called as programming errors. fatalError can be used to catch and indicate those. It helps you identify and fix issues during development by crashing the program immediately when such errors are occurred.

Preconditions are the situations when you have certain requirements or conditions that must be met before executing a code block, If any case those conditions were not met, you can return fatalError to notify it to the developer.

Security issues are the scenarios where security vulnerabilities or unexpected inputs could compromise the integrity of safety of the program. fatalError can be used as a fail safe mechanism. By crashing the program immediately, you prevent further execution that could potentially lead to security breaches.

If your program encounters a critical error or an unexpected state from which it cannot recover or continue executing safely those are called as unrecoverable errors. fatalError can be used to forcefully terminate the program. This is often done to prevent further damage or incorrect behaviour caused by continuing execution.

This is another common example where we can use fatalError to avoid production issues effectively.

public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

guard let newsCell = tableView.dequeueReusableCell(withIdentifier: NewsCell.cellIdentifier(), for: indexPath as IndexPath) as? NewsCell else {
return UITableViewCell()
}
if let story = self.viewModel.stories.value?[indexPath.row] {
newsCell.viewModel = NewsCellViewModel(with: story)
}
return newsCell
}

In UIKit, when we dequeueing an UITableViewCell for a UITableView we normally use the above code snippet. What usually happens here is, the compiler looks for an xib file with particular cell identifier and if it is exist, dequeue a new UITableViewCell as NewsCell. But mistakenly our xib file got deleted or missing and compiler is unable to find it, the guard let clause get failed and return an empty UITableViewCell . We have prevented the app crash on the production and error handling being done perfectly.

But the issues here is the user will always see a list with full of empty cells. There’s no way to detect this on development and you have to wait until your app to getting a bad review on the Appstore to get to know about this issue. Again we can prevent such situations using fatalError.

public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

guard let newsCell = tableView.dequeueReusableCell(withIdentifier: NewsCell.cellIdentifier(), for: indexPath as IndexPath) as? NewsCell else {
fatalError("Cell not exists in storyboard")
}
if let story = self.viewModel.stories.value?[indexPath.row] {
newsCell.viewModel = NewsCellViewModel(with: story)
}
return newsCell
}

Instead of returning an empty UITableViewCell , here we have returned a fatalError. Now as soon as you run the application in development, it will get crashed and the error message will tell you that your xib file is missing. Now you will be able to add the missing xib file and send it to the production. It will save your app from a production issue as well as set of bad reviews on the Appstore as well.

Where we shouldn’t use fatalError ?

You should always use fatalError to detecct erros which are on your control. Never use fatalError to detect the issues which are not in your control. As an example suppose you are getting some data from and API. You are getting userName via an API and it is an optional. It’s a best practice to safely unwrap this optional before use it as follows.

guard let name = userName else return {
fatalError("userName is nil")
}
nameLabel.text = name

Now what happens? If you are getting a nil value as userName from your API, when the unwrapping process you are returning an fatalError and crashing your own application. This is a suicide, don’t do this ever. The thing is API data is something out of our control. We can’t assure that back end sends valid responses every time. It’s not something you can guarantee from your iOS code base. This won’t help you to debug the issues in development time.

Same goes with the user inputs as well. User inputs also something we can’t control from our code base. It’s something coming from outside. You should never returns fatalError when validating user inputs. These are some recommended situations that you shouldn’t use fatalError.

  1. Recoverable errors: If an error condition can be recovered from or handled gracefully without terminating the program, it’s better to use Swift’s error handling mechanisms, such as throw and do-catch blocks. This allows for more controlled error propagation and graceful recovery instead of abruptly crashing the program.
  2. User input validation: When dealing with user input, it’s generally not advisable to use fatalError() to handle validation errors. Instead, you should provide user-friendly error messages or validations that guide users to correct their input or take alternative actions. Crashing the program due to invalid user input can lead to a poor user experience.
  3. Production code: In production environments, it’s important to prioritize stability and maintainability. Using fatalError() extensively in production code can lead to unexpected crashes and disruptions for end users. It's better to handle errors in a more controlled manner, log relevant information, and fail gracefully whenever possible.
  4. Testing and debugging: When writing unit tests or debugging code, using fatalError() can make it more difficult to isolate and troubleshoot specific issues. It's better to provide more descriptive error messages or use assertions to indicate failures during testing and debugging.
  5. Optional unwrapping: When unwrapping optionals, it’s generally better to use conditional binding or optional chaining with appropriate fallbacks or error handling mechanisms instead of relying on fatalError(). This allows for better handling of nil values and more predictable program flow.

Conclusion

Now you can realise that fatalError is really a powerful tool, which helps us to detect a lot of possible production issues at the development time. Also it will help you to prevent those as well. Especially it will help us to prevent programming errors in collaborative environment and make easier to work with a team. Effective usage of fatalError will save you from so many production errors and will make your life easy as a developer. If you do it perfectly you can depress your QA team as well. But be careful to use those in right situations. Don’t misuse it. Happy coding !

--

--

Tharindu Ramesh Ketipearachchi

Technical Lead (Swift, Objective C, Flutter, react-native) | iOS Developer | Mobile Development Lecturer |MSc in CS, BSc in CS (Col)