Debugging code: the guide to help you learn

NicolasBrondinBernard

Author
@NicolasBrondinBernard

Finding and fixing a flaw in code is learned through experience, but here's something to save you time!

Article published on 08/03/2022, last updated on 10/08/2026

One of the first frustrations in development is staying stuck for long minutes (hours) on a failure in our code, sometimes called a "bug".

If you want to know why I don't recommend using the term bug, I recommend reading my previous article.

Be that as it may, this is an unavoidable step in the life of a developer, and finding and fixing malfunctions in our code is a very important skill for gaining autonomy.

So I've tried to build you a guide to learn how to find bugs, understand them, and fix them, step by step:

Step 1: Understand

Read the errors

In most cases, reading the error (really reading it) and taking the time to analyze it will let you fix it very quickly, because the system (compiler, interpreter, environment) will try to give you as much info as possible about the failure in question.

SQL Example

SELECT * FROM movie WHERE title=`Mad Max`;
--ERROR 1054 (42S22) at line 1: Unknown column 'Mad Max' in 'where clause'

Here everything is given to us, the line, the exact position, and the type of error (1054 -> Syntax) and the reason for the error (Unknown column). Reading carefully, Mad Max should be a value, but it's surrounded by `backticks` instead of the expected 'single quotes' for values.

MySQL therefore interprets it as a column, hence the error!

Java Example

Exception in thread "main" java.nio.file.NoSuchFileException: players.dat
    at sun.nio.fs.WindowsException.translateToIOException(Unknown Source)
    at sun.nio.fs.WindowsException.rethrowAsIOException(Unknown Source)
    // ... more stack trace
    at java.nio.file.Files.readAllLines(Unknown Source)
    at java.nio.file.Files.readAllLines(Unknown Source)
    at Exceptions.getPlayers(Exceptions.java:12) <-- Exception arises in getPlayers() method, on line 12
    at Exceptions.main(Exceptions.java:19) <-- getPlayers() is called by main(), on line 19

Same thing in Java, where when an exception is thrown, the entire stack trace (the execution stack) that led to the error is displayed. That much information can be scary, but in reality if you break it all down, all the necessary information is right there.

Apparently the "getPlayers" method was supposed to open a file that doesn't exist (or has been moved, renamed); this file is called "players.dat".

JavaScript Example

Uncaught TypeError: undefined is not a function example_app.js:7
ExampleApp.initialize example_app.js:7
(anonymous function)

As in Java, the JS interpreter will give us the stack trace that needs to be examined, and discover which called function turns out to be undefined in our script.

A poorly loaded dependency, an empty object, etc... At least you have the keys to know where to look!

No error?

It may be that your software's malfunction doesn't cause an error but only an invalid state of the software, in which case your software doesn't specify its expectations sufficiently and doesn't check input and output data enough.

You'll then need to add conditions, try-catch blocks in order to create your own errors and prevent the invalid state received from being redirected into your application's normal execution path.

Reproduce

Being able to reproduce a failure is essential because it's the only way you'll have to gather enough information to find a solution, but also to check, to test, that your solution works as expected.

To reproduce a bug, you need to be able to recreate the state your software was in at the moment it appeared.

This state is represented by:

  • The version of your code at a given time T
  • The environment in which your software runs (OS version, hardware, available RAM and disk space, etc...)
  • The operations previously performed
  • The data passed to your code at the time of the failure.

By gathering as much of this data as possible, you should be able to put the application back (as much as possible) into the same invalid state and reproduce the bug.

No reproduction

If you can't manage to reproduce the failure, it means that you haven't put enough tools in place to analyze your application.

For that, make sure to set up:

  • Precise versioning of your code (essential)
  • An exception logging/backup system
  • A set of logs for your application
  • Monitoring of your environment

And wait for the failure to occur again, you'll then have enough info to detect it and reproduce it without any problem.

Intermittent reproduction

If you can't reproduce the "bug" consistently but only intermittently (even when injecting the same data), then the problem is likely caused by a factor external to your software, by your environment (missing permissions, the network, disk space, system timestamps, etc...).

Isolate

Just because you've managed to reproduce a malfunction doesn't mean you understand exactly what's wrong. In some cases, the problem is so unusual that you can't even pin down a possible cause.

To move forward in your investigation, you'll need to isolate the malfunction.

For now you simply have a faulty application, but the goal is to isolate as precisely as possible the file, class, function, or operation that's causing the problem.

To do this, follow the execution thread of your application step by step (with a debugger, exceptions, or failing that a series of logs) in order to trace back to the precise operation where the output result doesn't match the theoretical behavior and the input data.

Ideally, you'll end up with a specific line that you'll need to analyze and inspect rigorously.

Analyze / Frame

Now that you've managed to isolate the problem, you'll be able to try to twist it to learn a bit more, and especially try to multiply the number of faulty states you manage to generate.

An example: A function that transforms an integer doesn't return the correct result when you pass it the number 0 as a parameter. To frame this malfunction and understand its limits, we could test this function with parameters more likely to trigger an error, such as: 0.1, -0.1, 1, -1, null, etc...

If you manage to frame your problem, it will be easier to form informed hypotheses.

Make hypotheses

Hypotheses are targeted questions you don't yet have the answer to, but one of them should lead you to the right track, either by analyzing your code, looking for the answer yourself, or going to ask for help.

Example: when I sign up, I don't receive any emails, even though all my data is being sent to the SDK of the email-sending service.

Hypotheses and ways to check them:

  • The email might be going into spam => Check it myself
  • The email might never be sent => Check on the email-sending service's platform
  • The service might be temporarily down => Same
  • Am I using the right SDK method => Documentation
  • Why am I not getting an error? => Help forum
  • ...

Step 2: Search

Explain

For all the hypotheses you couldn't answer yourself, you'll need to do research, or ask for help. For research you'll need the best keywords/queries, and for help you'll need the clearest and most complete explanation of the problem possible.

One of the best methods for explaining a problem correctly is to go through a "rubber duck debugging" phase. This consists of explaining your problem to a plastic duck sitting on your desk.

This method will force you to explain your problem clearly, removing unnecessary details, and will sometimes even let you find the solution yourself, because your brain will be able to detect the inconsistent information you might verbalize out loud.

Otherwise, it will let you prepare your explanation to ask a colleague for help, on a forum, etc... and that's exactly the next topic.

Find help

In order, you can find help:

  • In the documentation
  • On Github (for open-source projects)
  • StackOverflow/Forums
  • Google/Blogs/Sites
  • Help groups/Slacks/Discords
  • Colleagues/Peers/Experts

If you want to know why this specific order, and what the right way to approach finding help is, I invite you to read this article:

Step 3: Fix

Implement

Do I really need to elaborate on this part?

Copy-paste (no). Rewrite, adapt, and above all understand what you're doing!

Test

We tend to quickly stop the "debugging" phase after implementing the fix, but in reality the phases that follow are the most important, and the ones that bring the most value to your code.

Manually testing your change with several valid and invalid test cases is a minimum, but to feel confident about the long-term reliability of your software, set up automated tests.

Adjust / Clean up

Once your code is implemented and tested, you can move on to the cleanup phase: removing logs, possibly superfluous code, etc...

But you can also make your code more efficient, more readable, more appropriate, all while being sure to avoid any regression thanks to your tests!

Document

Sometimes an error is simply a careless mistake, in which case this step won't necessarily be of much interest.

But if the malfunction comes from a calculation issue, an inaccuracy in a tool's documentation, or a quirk in the business logic you're working with, it's best to document your solution.

It can be a simple comment, information in a README, or an entry in an internal company knowledge base, but make sure not to skip this step, it might save your life in 6 months.

I hope this article was useful to you, and see you soon on the blog.


Elisa Ventur sur Unsplash

Finished reading this article?
Our complete courses
Take it to the next level with our courses!

Complete courses, exercises and certificates to really learn programming!

4.8 average rating

Comments (0)

to leave a comment

No comments yet