First things first…
How to write production quality code in 12 steps:
- Understand the problem.
- Divide the problem into smaller chunks if possible.
- Choose the data structures and algorithms to use for the solution.
- Write the code.
- Run the code. Be surprised if it runs on the first try. Otherwise, fix any errors the compiler has thrown at you.
- Test the program behavior. If it functions correctly, go to step 9.
- Guess which part of your code is responsible for the incorrect behavior. If necessary, add breakpoints or print statements to your code to pinpoint the bug.
- Fix the bug. Go to step 5.
- You’re almost there. Commit your code and open a merge request for code review.
- Assuming there’s a Continuous Integration (CI) process in place, see if some of the automated tests failed. If so, go to step 7.
- Your code has been deployed to the test environment. The product manager and the QA team test your code. If they discover any bugs you have missed, go to step 7.
- Your code is merged into the main branch and deployed to production. A user finds a bug that you, the product manager, and the QA team have missed. Go to step 7.
See any patterns above?
Hint: There are quite a few references to step 7.
You’ve probably seen programmers calmly writing code at 200 keystrokes per minute to solve hideously complex problems under strict deadlines, and the programs they wrote run perfectly on the first try, often only a few seconds before something blows up real good. Wait, what? I’m talking about Hollywood movies of course in which programmers can routinely achieve amazing feats while in the real world, we mere mortals have to do something called debugging, and do lots of it.
Debugging is often described as something separate from writing code as I did in the 12-step coding process above, but that’s not quite true. Unless you’re writing very simple programs, writing code involves a lot of trial and error, and it’s not uncommon to write, run, and revise a program hundreds of times before submitting it for review. In other words, debugging is done from day one.
Some software developers like to debug code, some don’t. As for me, I’m firmly in the like camp. I think of debugging as doing detective work, and unlike detectives who can successfully solve only some of their cases, our success rate is much higher, approaching close to 100% because we have one magic trick up our sleeves – the ability to reproduce bugs. If you can consistently reproduce the conditions that trigger a bug, it’s inevitable to find the reason because code is deterministic. It will behave the same every time. Granted, it may not be exactly deterministic when multiple branches of code are run in parallel, but there are ways to write well behaved parallel code.
Some software developers tend to fault compilers, databases, and other external components for errors in their programs, but almost all the time there is a fault in the programs they’ve written. If you have the steps to reproduce a bug, you’re half way finding what causes a bug. Just running the relevant sections of your code step by step and seeing how it behaves should be enough in most cases. Remember, determinism is your friend.
What causes bugs?
From most common to less common:
- Logic errors: In most programs, the most important part is the “business logic” as implemented in code. Either the logic is implemented incorrectly or it is implemented correctly, but some edge cases are overlooked. Extensive unit and integration testing is the key to identify logic errors.
- State errors: When a program starts to behave erratically, if restarting it solves the problem, the cause is likely to be the corrupt internal state of the program. Over time, the internal shared state may change unexpectedly when different parts of the program updates it. Using immutable data structures whenever it’s practical helps keep internal state consistent.
- Environment errors: These errors are caused by the environment a program is running in such as out of memory/disk space errors and network time outs. Some of these errors can be solved by simply increasing server resources.
There are many ways to debug a program, but the following are the most common:
- Print statements: Temporarily sprinkling a few print statements here and there to display variable values and track control flow is one of the oldest methods of debugging, but it’s still effective especially when debugging concurrent code.
- Breakpoints: Pausing program execution when it reaches a certain point, and then inspecting variables as you step through the code line by line can be quite useful to understand code behavior. For debugging concurrent code though, use print statements since you can’t really step through concurrent code.
- Logging: When you make your print statements more descriptive, categorize them by levels of severity such as INFO and ERR, and store them permanently in an external log file or database, what you get is logging – an indispensable tool to trace code execution flow. Logging comes handy for other purposes such as user analytics, but that’s outside the scope of this article.
- Version Control: With version control systems like Git, you can easily revert to a past version of your code if one of the recent changes you had made introduced a bug.
- Profiling: If you are having performance problems, you can use profiling tools to discover in which functions your program consumes CPU time the most.
It’s hard to get good at programming without getting good at debugging since debugging is such an essential part of the process. Debugging the code others have written is also a great way to learn how an unfamiliar code base works. Granted, debugging isn’t always pleasant, but the better you get at debugging, the faster you can write code – maybe not as fast as our Hollywood programmers, but we can only hope!