Debugging Techniques!

Squish those bugs.

Introduction

It is inevitable that when you are writing code you will end up with bugs. If poorly managed, the time you spend debugging your code could be greater than the time spent actually writing it. There are various things you can do to help minimise this however.

Various development environments contain tools to help identify bugs. I will not discuss those here but instead look at general techniques which may be applied no matter the language you are developing your software in.

The code examples on this page are in a fictional language called RyanScript. I have done this deliberately as there are many programming languages out there, each different in their syntax. The concepts discussed on this page are generic and apply no matter which language you are developing your software in.

Three types of errors

There are three types of errors you will encounter.

  • Syntax errors
  • Logic errors
  • Runtime errors

Syntax errors are when you have written part of your code with a mistake. It could be a spelling mistake or a missing character for instance. A very common syntax error is to forget the semicolon at the end of a statement. Another common mistake is to misspell a variable name.

These errors are often quite easy to fix as the compiler or interpreter will halt with a message telling you which line the syntax error occurred on and what it was expecting.

Logic errors involve your code being syntactically correct but it does not behave how you would expect it to. Maybe a certain action is to occur when a variable gets above a certain value however when you run the program that action does not occur unless the variable is exactly that value.

These errors can be difficult. Identifying the section of code responsible can be tricky and you won't get an error message. It may also be the case that the software functions properly most of the time and only works incorrectly under certain circumstances.

Runtime errors occur when a situation occurs during the running of your code which results in the software not being able to continue execution. Exampes of situations which may cause this are a divide by zero or the software trying to open a file which does not exist.

These errors are sometimes easy to identify and sometimes difficult. If it is an error regarding a missing or incorrectly named file for instance identifying the cause of the problem and the fix should be easy. If it is something like a divide by zero then working out why it occurred may be easy but working out how to prevent it happening may be trickier. These errors can often be caused by logic errors and similarly often occur only sometimes and under certain circumstances.

Prevention is better than the cure

We will look at various techniques to identify and squash bugs below. Before we do so, let's look at some things we can do to minimise the number of bugs we produce in the first place.

Taking time to design your software in a neat and logical structure before you even begin coding will lead to your code being cleaner and less prone to errors.

Once you start coding, if you follow Good programming practice you will also write code which is less prone to errors and easier to debug when those errors do occur.

Utilising libraries and built in functions whereever possible can also help as you will be making use of code which is already tested and solid.

Although generally a good idea to make use of libraries where possible there is a line of thinking that in certain circumstances where performance is important, writing your own code can be leaner and more efficient. This is particularly the case when writing client side web based applications.

Testing

There are two general categories of testing. Black box testing and white box testing.

Black box testing involves testing the code, or part of the code (Eg. a function) without actually looking at the code itself. We run the product giving a variety of different inputs and ensure that it gives the required outputs and behaves the way it should. If something goes wrong we generally don't make any assumptions about why, we just note that it did. Because of the nature of black box testing it can easily be done by an end user. This can be effective as they are more likely to use it in ways that the developer never thought of.

Black box testing gets its name as the internals or the actual processing is dark to the person doing the testing. They can't see it.

White box testing involves testing of the code whilst being able to see the processing and interact with it to better understand what is going on. This is usually done by a developer and often after black box testing has revealed bugs which need to be investigated. The developer has a variety of tools at their disposal to help them including CASE tools and the debugging techniques discussed below.

Testing can show you that there are bugs. It cannot prove there are no bugs. Good testing will increase confidence in the quality of your code but never assume there are no bugs (except for small, trivial programs).

Error Messages

There is an acronym in IT which is RTFM, Read the _______ Manual. (I'll let you figure out what the missing word is.) (I don't expect to hear you say this word either) I would like to propose a similar acronym RTFEM. The last two words are error messages. I don't mean just skim the error I mean really read and consider what it is saying. This might seem obvious but I get an unusually large number of requests from students for help with their code and when I ask them if they have read the error message they say no.

Debugging Techniques

Debugging output statements

One of the easiest and most effective ways to start debugging your code is to start printing things out. Printing a message within branches of an IF statement will help you determine which branch is actually being executed. Let's say we have a simple program which asks you for a score, adds 10 to it and if the score is above 20, saves to the database.

wrong_branch.rs

  1. myscore = input('Please enter your score: ');
  2. myScore = myScore + 10;
  3. if (myScore > 20) {
  4. save_to_high_scores(myScore);
  5. }
  6. playAgain = input('Play again? ');

When you run this program however, no matter what the score entered is it never saves to the database. You could decide that the problem must be with the database and go looking for a bug within the function save_to_high_scores. It would be smarter to test that the function is actually being called first though. Let's put a debugging output statement in and make sure it works:

wrong_branch.rs

  1. myscore = input('Please enter your score: ')
  2. myScore = myScore + 10
  3. if (myScore > 20) {
  4. print ('Saving to database');
  5. save_to_high_scores(myScore);
  6. }
  7. playAgain = input('Play again? ');

Now we run the program again and notice that the if statement never seems to be entered. This tells us that the problem might not be within the function but is happening somewhere within the current code. Now we might decide to try another strategy which is to print out variables to track how they are changing throughout execution of the code.

wrong_branch.rs

  1. myscore = input('Please enter your score: ');
  2. myScore = myScore + 10;
  3. print ('myScore is: ' + myScore);
  4. if (myScore > 20) {
  5. save_to_high_scores(myScore);
  6. }
  7. playAgain = input('Play again? ');

We run this and notice that the value of myScore is 10 no matter what we enter. This is perplexing. When this happens, a good strategy is to progressively move the print statement up through the code or to add multiple print statements so you can see a history of how the variables value changes.

wrong_branch.rs

  1. myscore = input('Please enter your score: ');
  2. print ('myScore is: ' + myScore);
  3. myScore = myScore + 10;
  4. print ('myScore is: ' + myScore);
  5. if (myScore > 20) {
  6. save_to_high_scores(myScore);
  7. }
  8. playAgain = input('Play again? ');

Now we realise that myScore is zero even before running the addition so the problem is not on line 3. It must be on the first line, and on closer inspection we realise that we have made a typo and written myscore instead of mySscore. Problem solved and the potential to waste a lot of time looking in the wrong location for the but has been averted.

Flags

Flags are useful in conjunction with debugging output statements. A flag is a variable that we set to indicate that a certain section of code or event has occurred. eg. maybe we wish to know if a particular branch of an if statement has been entered or if a particular function has been called. Alternatively we may wish to know how many times a loop has run. We will create a variable, which may be a boolean if we just want to see if some code has been run, or an integer if we want to know how many times a loop has run. Then we will use a debugging output statement to print the variable to help us understand what has happened.

high_score.rs

  1. score = read_score_from_db ( userID );
  2. level = highest_level_reached ( userID );
  3. bonusFlag = False; # this is the flag
  4. if ( score > 500 and level < 10 ) {
  5. bonusFlag = True;
  6. score = score + 100;
  7. }
  8. print ( bonusFlag );

Commenting out sections of code

An important stratety in debugging is divide and conquer. When searching for a bug we want to progressively identify sections of code we are certain the bug is not in to help us narrow down our search. Sometimes there is processing which we can remove and still have the rest of the code function. Consider the following code which reads user details from a database, orders them in a particular way, then prints them to the screen:

users_print.rs

  1. users = get_users_from_db ();
  2. users = sort_by_employment_level (users);
  3. print_users (users);

When we run this code only the first two users are printed to the screen. The problem could be in any of the three functions called above. We make an assumption that the second function could be the culprit so we comment it out. If the users are all printed (though not in the right order) then we can assume that the bug is probably in the function sort_by_employment_level and start looking there.

users_print.rs

  1. users = get_users_from_db ();
  2. # users = sort_by_employment_level (users);
  3. print_users (users);

This debugging technique is only useable when we can comment out code and still have the rest of the code function properly. If it is not possible to do this then a driver or stub might be what you need.

Drivers

A driver is a small piece of code which allows you to run another piece of code in a managed way. This can be useful for testing a function we have written. Lets say we have a function which accepts an array of integers as its parameter and sorts them in desending order. We have just written it and wish to test it as it is essential in the running of other code we wish to write next. We want to run it in a managed way which will allow us to re run the tests several times easily as we iron out any bugs we find. Here is an example of what a driver might look like in this scenario:

test_sorting.rs

  1. function sorting_driver () {
  2. print ("Test 1: 5, 4, 7, 1, 8");
  3. print ( sort_array([5, 4, 7, 1, 8]);
  4. print ("Test 1: 3, 8, 2, 9, 8");
  5. print ( sort_array([3, 8, 2, 9, 8]);
  6. print ("Test 1: 1, 2, 3, 4, 5");
  7. print ( sort_array([1, 2, 3, 4, 5]);
  8. }
  9. function sort_array (numbers) {
  10. #code here that is probably buggy
  11. }
  12. # main();
  13. sorting_driver();

Picking the right tests to run is important in the effectiveness of your driver. You want to make sure you think of possible scenarios in which the code might break but also in which it does work to help you narrow down what the problem might be.

Stubs

A stub is the opposite of a driver. Whereas a driver sits above the code you have written and allows you to run it (ie. drive it), a stub takes the place of code which has not been written yet. Stubs allow you to test code which relies on functionality which you have not yet implemented. Lets say you have written some code which prints the top 10 scores for a game you are developing. The scores will be stored in a database but you haven't written the functions for reading from the database yet. Instead of calling the function which would read from the database to retrieve the data, we can instead call a function which will just return some static data for the purposes of testing if we can render the data properly.

test_top_ten.rs

  1. function render_top_ten () {
  2. # var topTen = get_scores_from_database();
  3. var topTen = get_scores_from_stub();
  4. # code to render the top ten scores
  5. }
  6. function get_scores_from_database () {
  7. # code not written yet
  8. }
  9. function get_scores_from_stub () {
  10. return [[5, 'Hitman'], [23, 'Denogginator'], [45, 'WiseGuy'], ...];
  11. }

Desk checking

Using the above techniques, we think we have narrowed down where the bug is in our code. Now we need to work out just what is happening.

Desk checking involves manually working through a piece of code and tracking the values of variables as we go. It is a particularly effective debugging technique when we have a piece of code that is manipulating data.

We perform our desk check as a table with the column headings being the names of the variables. If we had the following code which finds the highest even number in an array of numbers:

dummy_processing.rs

  1. function get_largest_number (numbers) {
  2. var largest = 0;
  3. for ( i in numbers ) {
  4. if ( i % 2 == 0 ) {
  5. if ( i > largest ) {
  6. largest = i;
  7. }
  8. }
  9. }
  10. return largest;
  11. }

We would step through the code ourselves line by line and produce the following table:

numbers largest i i % 2 return
[4, 5, 2] 0 4 0  
  4      
    5 1  
    2 0  
        4

Notice that the values within the columns have been entered with gaps where appropriate to show the chronological order of operations. This makes it easier to trace back through the processing if required.

Sometimes it's useful to include columns which aren't variables but are specific bits of processing. The fourth column above isn't a variable but is processing that we want to ensure works properly so we have listed it as something to track. There aren't any specific rules about what we do and don't enter for our columns but generally the more detail the better. All variables should be listed as a minimum though.

Picking the right test data is important in maximising the value of desk checking. Sometime the processing will work under some conditions but not others. In general you want to make sure you check boundary values and differing scenarios. eg, for the following code we may decide to desk check with the following sets of test data:

  • [2, 4, 6] - all even numbers, last number is largest.
  • [8, 2, 4] - all even numbers, first number is largest.
  • [2, 8, 4] - all even numbers, middle number is largest.
  • [4, 8, 6, 8] - what if there are multiple of the same number?
  • [3, 7, 4, 11] - only a single even number.
  • and so on.

When desk checking multiple sets of inputs you can check them in separate tables or to save yourself writing out the titles every time you can just list them on one table directly under each other. Separate the different tests with a horizontal line.

When desk checking it is good to know what the expected output / result should be for a given input. Sometimes you can tell just by looking at the data what the result should be, sometimes you may need to work it out yourself. If you have created IPO tables for the processing as part of understanding the problem then referring to those can help you define the expected results.

Move code into a smaller program

Another technique to use once you think you know where the bug might be is to play about with a simpler piece of code in another file.

Sometimes it is useful to verify different sections of code to help you narrow down where the problem is. If various sections of code may be run independently then copying that code to a separate file and testing it by itself can make it easier. You may need to add in some stubs, drivers and debugging output statements but it allows you to be certain that errors aren't being caused by something somewhere else in the code.

For instance, maybe we are making a game and the character is not responding to our button presses. Is the error in recording events (keyboard presses), processing the actions or rendering the new data? We can start by copying the code for recording events into a new file and getting it to set a flag according to the inputs then print that flag. If this works we may move on to look at the processing section of the code. If it doesn't work we can easily play about with it and experiment without other bits of code getting in the way.

Peer checking

It is common for people to overlook, and keep overlooking their own mistakes. It is often the case that the error is simple and obvious but because you have a particular train of thought (the same train of though that lead to the error in the first place) you just keep overlooking it. Getting someone else to have a look over it can be very powerful. It can be even more effective if that person is not invested in the product and has not seen the code before. This is called the Uninterested observer principle and can be very effective. Don't overlook this valuable technique or think you shouldn't use it out of fear that others may doubt your skills.

Give it some time and come back

This is another simple yet very effective debugging aid. It is common to spend ages looking at a problem and get nowhere. Leave it for a period of time and come back to it and identify the error near instantly. It is also common that the reason for the bug comes to mind whilst doing something completely unrelated. Don't underestimate the power of this and if you are nowhere with a bug it is often much better to do this than persist with banging your head against a brick wall. Often the more you persist with a bug the more stressed you get and the more stressed you get the less flexible and creative your mind becomes which is the exact opposite of what is required for good bug squashing.

The right mindset

You will be more effective whilst utilising the debugging techniques discussed above if you are in the right mindset. A calm, methodical and open mindset whilst debugging will make the experience more effective.

The big picture

It's a rare person that enjoys debugging. Still it is something which needs to be done to ensure the quality of your code. It would be a shame to have gone to all the time and effort of understanding the problem, create a solid plan and build the solution only to have the result not work very well due to bugs. Even small, inconsequential bugs, will affect the confidence users have in your product so it is vital that this stage is done well.

Summary

Debugging Output Statements
Print the values of variables to verify your assumptions about the processing.
Commenting out sections of code
Reduce the processing to narrow down where bugs might be.
Drivers
A small piece of code which allows you to run and test another section of code under controlled conditions.
Stubs
A placeholder for another section of code. Usually returns hard coded values.
Flags
A variable that is set to indicate particular code has been run or scenario has occured.
Desk Checking
Manually working through your code by hand.
Move code to a smaller program
Work on a section of code in isolation to make things simpler.
Peer Checking
A fresh set of eyes will often spot things which you miss.
Think about your data
The right test data will make it easier to spot and undertand bugs in your code.
Look for the details
Don't just see that there is something wrong. Look for the details which will help you understand why it is wrong.
Calm and Methodical
Debugging is more effective with the right mindset.