( This post is inspired by two fantastic books: “Think like a Computer Programmer” (Spraul), and “Debugging” (Agans). They’ve absolutely made me a better developer – I highly recommend them. )
For new developers, the most frustrating aspect of development is often when your application doesn’t do what you want it to do, and you don’t know why.
You have a clear vision for the feature you want to build. You’ve come up with a creative solution to the problem at hand. You’ve crafted clean, elegant code that you’re proud of.
But, somehow… it just doesn’t work.
My Story – Turning Debugging Frustration into An Enjoyable Process
As a fledgling dev, I found debugging particularly frustrating. I concluded early on that I was not a natural debugger.
The problem: I was always trying to be “too clever”. When I hit a bug, I’d over-analyze the system I was working on, and try to come up with some lightbulb insight by force of will.
Or — I’d “just know the problem is in this piece of code” – and then wrestle with a particular function or file for a couple of hours.
All too often, I’d be wrong. Not only would I be disheartened that I didn’t really understand the system, I’d waste time and energy refactoring code that was unrelated to the bug.
Debugging is a Skill – Some are Naturals, Others have to Learn It
You may know a developer who is a natural bug hunter. When faced with a system that doesn’t work, they have an uncanny ability to intuit their way to the root of the problem and fix it.
Somehow, they home in on the real problem and fix bugs rapidly. Amazingly, they don’t get frustrated – they seem to actually enjoy the process.
This is because debugging is a mindset and a few simple habits. If you’re not a natural debugger, you can learn.
It’s not difficult – just deliberately implement a few simple habits – and you’ll transform this frustrating aspect of development into a methodical process of small wins and progress. Debugging can become be a challenge you actually relish.
Dysfunctional Debugging – Banging Your Head Against the Wall
Let’s quickly cover how not to debug your code:
- Hoping and wishing for the bug to jump out and reveal itself
- Being too clever: forming grand theories and epic intellectual treatises about what’s happening
- Incorrectly assuming the problem is in X area, and wasting hours on a deep-dive into X
- Shotgun debugging – changing too many things at once, wading knee-deep into a mess (even introducing new bugs)
All these approaches lead to frustration and confusion. There is a much better way.
The Debugging Mindset – Take the Sting Out of It
Let’s quickly cover the frame of mind that makes debugging go smoothly…
Accept Debugging as Part of Development
They say we get dissatisified when reality doesn’t meet our expectations. I find this very apt for debugging – you have a beautiful vision for your app, you’ve written the code that should do what you want it to do – yet, it just… doesn’t… work.
But if you accept debugging as an integral part of the development process, something changes.
You account for it. You come to expect to spend time ironing things out after you’ve written that function or test. And when it’s expected, it’s not nearly as painful.
Remember to Apply The Debugging Rules
When you’re in the midst of a wrestling match with your code, it’s all too easy to get tunnel vision and forget to tackle things methodically.
It’s a vicious spiral – the more frustrated you get with a bug, the less you can calmly observe, investigate, and track down the root cause.
Personally, I have a summary of my debugging rules printed out (yep – paper!) and within arm’s reach when I write code. As soon as I notice I’m stuck on an issue for more than ten minutes – or if I feel frustration levels rising – I reach for the cheatsheet.
It really helps me reset, clear the mental cache, and hunt the bug methodically and efficiently.
Here’s a summary of my debugging approach. As mentioned, it’s informed by my two favourite books on the subject – “Debugging” (Agans) and “Think Like a Computer Programmer” (Spraul).
- Look before Leaping In
- Get Visibility
- Tinker – Change One Thing at a Time
- State the Problem Explicitly
- Search and Destroy
- Review your Build
Note: These rules are written in order. That is, you should try the first ones first, as they require the least work.
If the bug persists, you move onto the later ones.
80% of bugs will roll over and submit with a good dose of 1), 2) or 3). Tougher critters take a little more whacking, and that’s what the later points are for.
1. Look before Leaping In
The most important rule, and the one you should always apply first.
Before diving in to fix things… before even coming up with ideas as to what’s wrong… you should simply just LOOK at what’s happening.
Read the code you’ve written. Read the input. Read the output. Read the error message.
It’s all too easy to dive in and start rewriting code.
But it’s far more important that you simply look and observe the parts of the system that matter: the code you’ve just written, the code that calls it, the input, and the output.
Bugs are frustrating because we expect the code we’ve written to return X, but in fact it returns Y (or returns nothing, or fails, etc).
A bug is a signal that the code we’ve written isn’t actually doing what we think it does. This is often revealed by simply stepping back and looking at it, free of assumptions.
Let yourself just take a breath, and read it. You’ll be surprised how often you just notice what’s wrong.
It can be as simple as a syntax error, the wrong method call, or badly ordered logic. By looking before leaping in and chopping your code up, you can catch these easy bugs in seconds.
2. Tinkering. Change one thing, run your program. What’s different?
Tinkering is in my opinion the bread and butter of debugging.
Tinkering takes the frustration out. It’s a steady, methodical process of trying something new, and seeing what changes.
It’s a mini-process of elimination, with small wins along the way. Every time you change something, you get some new information, or eliminate that piece of code as the cause. Or, you actually find the bug!
Tinkering is much less “figuring it out”, and closer to “pull the levers and see what happens”. It’s about probing the system, varying the input or logic, and seeing how the system responds.
Here are some useful ideas for tinkering with your code when it’s not working:
- Comment out code that you think caused the bug
- Comment out code you are “sure” works
- Swap a chunk of logic for a hardcoded value
- Swap a function for a dummy function
- Swap logic for what it “should” evaluate to
- Vary an input. Try an empty array/string
- Vary a type. Make an int a string, or vice versa.
- Vary authorization. Run it as user, as admin, or logged out
- Simplify input – feed the most minimal dummy array or string through the system
- Try input that “definitely works”
- Try input that “definitely doesn’t work”
To tinker is to prod the system, learn how it responds, and gain new information.
Tinkering is also about reducing complexity, so you don’t get distracted by complicated logic, when the source of the bug is simpler but elsewhere. Tinkering helps you stay on course to track it down.
Remember to only change one thing at a time, and change it back before you try the next thing.
3. Get Visibility Into Data Flows and Storage
This one boils down to see what’s actually there, and see what is actually happening.
Rather than guessing, at what some opaque part of the system is doing, it’s much more useful if you can actually open the box and take a look inside – see what data is actually being stored or is flowing through.
See what’s actually being written to the database.
See what data is sent in the request.
Read the response.
Log state to the console when it gets set.
Have events trigger on function calls and check the logs.
There are many great tools and debuggers for getting visibility.
However it’s not always easy to see the state you need to see. You may have a “black box” in your system.
In these cases, get visibility where you can – before the data enters the black box (perhaps in a form submit on the front end), or after it’s been processed and stored (e.g. on the blockchain or in the database)
Anywhere you can get visibility will shed valuable light on what’s there and what the code is actually doing.
4. State the Problem Explicitly
Whatever problem you’re wrestling with, you’ll get more clarity by expressing it.
Put the problem into words – summarise it for a co-worker, or write it down in 2-3 sentences.
Keep it short to force yourself to take a zoomed-out view. Expressing the problem helps you synthesize your thoughts so far, and can shed light on possible root causes.
It’s best to express the problem in the form of expected vs actual – don’t get lost in theories about causes. Just say what it should be doing, what it’s actually doing, and which parts seem to be working properly.
Stay away from theorizing or guessing about the “why” – the goal here is just to externalize the raw problem, either verbally or in writing.
Doing this defuses frustration and opens the door for a solution to pop up, or at least new avenues to pursue.
5. Search and Destroy
If you’ve got a stubborn bug in a system architecture with several layers or components, it’s time to get thorough. The most frustrating and unproductive thing you could do is spend hours wrestling with a part of the system that is working just fine, while the bug laughs at you from it’s hiding place elsewhere.
We need to systematically home in on it’s location by quickly ruling out areas of the system it could be in.
Here’s an example. In dApp development we have the blockchain, the contract, the web3 interface, the front-end logic, and the UI.
Data flows in both directions, end to end – from the blockchain to the UI, and from the UI to the blockchain.
Divide the data flow into sections. Like this:
- User fills in form and clicks ‘Submit’
- A front-end JS handler function fires in response to onSubmit, calling a web3 representation of a contract function
- Web3 sends the data to an Ethereum node over a JSON-RPC request
- The Ethereum network mines the transaction in a block, the computation is performed, and the state of the contract is updated
- The Ethereum node responds with the transaction confirmation
(You could break this data flow into even smaller sections – but these are the major demarcations needed for debugging a transaction).
Each step has data input and output.
Commence Search and Destroy
Start with the “most likely” section of the data flow, and attempt to rule it out as the home of the bug.
Does the form submit the correct data? Then the form is working.
Does the front-end handler function pass the right data to the contract call? Is it calling the right contract function? Then the handler is working.
Is your web3 call successfully sending the JSON-RPC request to the Eth node? Is it sending the right data? And so on.
The goal is to quickly rule out sections of the data flow – firstly, is it working? Secondly is it receiving and sending the right data?
If a section looks clean, move on and check the next most likely section.
When you find one area that is failing or sending bad data, only then do you dive in and start exploring and tinkering to find the bug and fix it.
But you have to find the section of the system it lives in before you get the tools out.
Finding the home of the bug is the quickest way to slash your debugging time, and avoid wasting hours of effort in the wrong areas.
6. Review Your Build
“What have I built? What do I expect it to do? What should a basic working version of this look like and do?”
Answer these questions, and then revisit your initial specification / design document.
Reviewing what you’ve built so far is useful when you’re deep in the trenches of a project. It helps you zoom out, see the bigger picture, and connect the dots.
Asking these questions can synthesize your understanding of what you’ve done so far – sometimes, bits of information pop up that can help you solve a bug. When you review your build, you often remember that you had a place-holder value in a function, or that you’re using a hosted CDN library and never got around to installing a package, etc.
Sometimes, these forgotten patches or temporary hacks end up interacting with new code in unexpected way and making it fail.
Debugging Tools Recap
- Look before Leaping In
- Tinker – change one thing at a time
- Get Visibility
- State the Problem Explicitly
- Search and Destroy
- Review Your Build
With These Tools, Debugging is Fulfilling
Most of the pain of debugging comes from confusion and frustration. Confusion when you don’t know why the system is failing, frustration when you put in a lot of effort but it gets you nowhere.
The principles above are the antidote. Applying them consistently makes debugging is a process of small wins – discovering new information, ruling out hypotheses, and systematically honing in on the root cause. Debugging becomes a rewarding investigation, as you steadily converge on the bug and then fix it.
So there you have it. That, IMO, is how to debug. May you squash many sneaky bugs, and build awesome things!