Finding the root cause of an error in your app can often feel very intimidating, whether you’re brand-new to programming or you’ve been building coding for decades. Debugging problems can be extremely time consuming. Where do you start looking? How do you know if what you think is the problem is actually the problem?
While there is no one-size-fits-all approach to debugging, there is a major guiding principle that can be extremely helpful in determining the root cause of an error, and that is to apply the scientific method to your analysis. The Scientific Method is a process we use to learn new things, analyze why things happen, and correct misconceptions. In simple terms, the steps involved are:
- Ask questions
There are many ways to observe problems in your app. Maybe it crashed. Maybe it didn’t do something you were expecting it to. Maybe your users or QA testers observed some incorrect behavior. Maybe you saw something yourself. Regardless of how it happened, debugging always starts with an observed issue.
Questions form the basis of debugging (and indeed, all learning in general). Depending on your goals, deadlines, or other constraints and circumstances, your questions may range from “why is this happening and how do I fix the root cause?” to “how do I hide the symptoms of this issue?”
There are no wrong questions here. Some questions may ultimately lead to irrelevant information, but the process of asking these questions helps train your mind to ask better questions in the future.
It’s also possible that your observations lead you to areas where you don’t have the required domain knowledge to even know what questions to ask. In situations like this, you should talk to other developers and ask “what questions should I be asking?”. Try to not ask for the answer, but instead ask for the question. Getting an answer gives you a single point of reference. But getting a question gives you a trajectory for future learning.
Once you’ve formed a question, next comes a very fun part: you get to make up an answer! The answer you invent can be anything, as long as it is testable. For example, you could hypothesize that the reason your app crashes is because the Moon was exactly at its orbital apogee. This is unlikely to be the root cause of your app’s crash (unless you’re writing an astronomy app?), but the point is that your hypothesis can be entirely made up.
Over time, experience will teach you to recognize patterns. For example, experienced Objective-C developers have long recognized that an
EXC_BAD_ACCESS crash is likely an error related to memory management. Swift developers know what a crash due to force-unwrapping
nil looks like. The more practice you get at debugging, the easier and quicker this process will become.
After you’ve formed a hypothesis, now you get to perform an experiment. In order to analyze the hypothesis, you need to devise a way to either prove or disprove your theory. Sometimes, this is really trivial: run your app again, set a breakpoint, and check to see if your variable is
nil when you try to unwrap it.
Sometimes the process is a lot more complex. This is where the developer tools can be invaluable.
At the most fundamental level, we have the ability to print messages to the console. This is often referred to as “caveman debugging“, because it’s using the simplest and most fundamental tools. Sometimes this is good enough. But
NSLog() can be tedious tools to work with.
Fortunately, Xcode provides some really useful debugging tools beyond
NSLog(), like breakpoints and watchpoints. The debug gauges in the Debug Navigator can help you observe the state of your app. If you see memory usage continuously increasing, then you have Observed Something and may need to consider Asking More Questions. You also have more advanced tools available, like the LLDB console, everything in Instruments, and even hyper-specialized tools like
These tools help you analyze the results of your experiment. The experiment you construct should ideal follow proper procedures and allow you to test your variables in isolation, as well as having a control.
Ideally, these experiments and analytic tools help you prove your hypothesis correct. If they do, then it’s time to start figuring out how to solve the problem. Often, testing proves our hypothesis false. We prove that what we thought was the issue wasn’t actually the problem. When this happens, we go back to step one: we’re still observing the problem, which means we need to ask new questions, form new hypotheses, and devise new tests to examine the theories.
And of course, these tests you devise should ideally be captured in the unit tests of your app, to help you guarantee that the problem doesn’t resurface in the future as other code changes!
This pattern of Observe-Question-Hypothesize-Test is extremely powerful. It doesn’t always work for every kind of problem (such as problems that are inherently not reproducible), but when it’s applicable it is an excellent way to organize your thoughts and know that you’re on the right path towards solving your app’s problems and becoming a better and wiser developer.
So the next time you’re stuck on a bug and don’t know how to proceed, consider applying the scientific method.