Why #if DEBUG Conditional Should Be Avoided in Library Code

Conditional Compilation along with Active Compilation Conditions is a way to alter the app’s behavior depending on the build configuration. For example for the code to compile only in the Debug configuration, we can do:

#if DEBUG
  performAdditionalChecks()
#endif

In this article, I’ll show why these kinds of conditional blocks shouldn’t be used directly in the source code of libraries distributed to other developers.

Problem

Around a week ago I noticed that my DeallocationChecker library didn’t catch a leaking view controller in a project I help with. At the time the main entry method in that library looked like this:

public func dch_checkDeallocation(afterDelay delay: TimeInterval = 2.0) {
  #if DEBUG
    // Real logic can be ignored for this article
    if isLeaked {
      assertionFailure()
    }
  #endif
}

After a short debugging session I noticed that even though the app is running in the Debug configuration, the code within this #if DEBUG block isn’t compiled at all! It confused me because that code was running fine just a few months ago.

Here’s a summary of the situation:

  1. Active Compilation Conditions were set up correctly in the main project because the code in #if DEBUG blocks inside the project was being compiled and executed.
  2. DeallocationChecker was included through Carthage.

So, there was something wrong with conditional compilation using DEBUG flag specifically in the project included through Carthage.

Carthage

In contrast to libraries imported through CocoaPods or as subprojects, Carthage operates in a two-step way:

  1. First, when we add or update a dependency, Carthage builds it and produces a binary framework.
  2. Then, going forward we use that prebuilt framework in the project.

This means, that when Carthage builds a framework, it doesn’t know whether the framework will be used in Debug, Release or any other configuration! So, it defaults to the Release configuration, causing that #if DEBUG block to not be compiled. We end up with our app’s code running in the Debug configuration, yet the library’s code is at the same time running in its Release configuration.

Technically speaking, we can instruct Carthage to use the Debug configuration but that complicates the workflow significantly. We would have to build both configurations and make sure to not ship a non-optimized Debug version to our users.

Also, the same rule applies to libraries distributed only in a binary form, e.g. Fabric.

Solution

A better approach to conditioning the execution depending on a configuration is to get the current configuration from a library’s user. It can be as simple as:

#if DEBUG
  let library = YourAwesomeLibrary(useDebugChecks: true)
#else
  let library = YourAwesomeLibrary(useDebugChecks: false)
#endif

This way, as a library’s author we stop being dependent on the exact way a user builds it. The intention gets explicit.

Summary

We learned how a subtle difference in a build process can break an otherwise sound code. What’s worse in this case, due to the nature of DeallocationChecker, which is invisible unless there’s a leak, the bug went unnoticed for some time.

As to why the conditional check stopped working for us just in recent months? We switched from CocoaPods to Carthage in the meantime. So, DeallocationChecker didn’t suddenly break, it just never worked correctly with Carthage.