CS 70

Debugging Your Code

The Six Stages of Debugging

  1. That can't happen.
  2. That doesn't happen on my machine.
  3. That shouldn't happen.
  4. Why is that happening?
  5. Oh, I see.
  6. How did that ever work?

— $mike, Comment on “Bugs Are Magic Tricks”

Debugging is one of the most important skills you'll develop as a programmer. It's also one of the most challenging, because it requires you to figure out not just what's wrong with your code, but how you're wrong about your understanding of what the code is doing_

The Debugging Mindset

At the heart of every bug is a mismatch between what you think should be happening and what actually happens. Your job when debugging is to discover your own misconception.

Debugging is hard work—you have to question your own assumptions and be willing to be wrong about things you felt certain about. But it can also be deeply satisfying work, because each bug you fix teaches you something new about how programs really behave.

  • Hedgehog speaking

    This sounds really hard…

  • LHS Cow speaking

    It sometimes is! But debugging is a skill you build with practice. Every bug you fix makes you better at spotting similar issues next time.

  • RHS Cow speaking

    Building a good debugging toolkit and developing a systematic approach can make the process much more manageable.

Your Debugging Toolkit

Before we dive into a detailed example, here's a quick overview of the tools and techniques available to you:

Print Debugging
Adding std::cerr statements to see what your code is actually doing. This is the workhorse of debugging—simple, flexible, and surprisingly powerful. (Use std::cerr rather than std::cout for debugging output; for one thing, std::cerr is unbuffered, so you see the output immediately.)
Valgrind
A tool for detecting memory errors like out-of-bounds accesses, use-after-free, and memory leaks. See our Valgrind guide for details. Valgrind provides much better evidence than a bare segmentation fault.
Assertions
Statements that check your assumptions and crash immediately if they're violated. See the assert guide for when and how to use them effectively.
Compiler Warnings
Always compile with -Wall -Wextra -pedantic to catch potential issues early. The compiler knows a lot about common mistakes!
Reading Backtraces
When your program crashes, the error message usually includes a backtrace showing the sequence of function calls that led to the crash. Learning to read these is invaluable.
Rubber Duck Debugging
Explaining your code out loud (even to an inanimate object) forces you to articulate your assumptions, which often reveals the bug.
  • Duck speaking

    Why would I explain things to a rubber duck?

  • LHS Cow speaking

    Because explaining forces you to question your assumptions! Often you'll figure out the bug mid-explanation. It's not about the duck listening—it's about you having to put your thoughts into words.

Another Tool: The Right Perspective

As we've said, one thing that makes debugging hard is that you have to question your own assumptions. One way to do so is to apply what we call the expanding island of certainty principle.

When your code behaves in ways that seem baffling, it can make you question everything you thought you knew about how your code works. Something you currently believe is actually false, but what is it? Figuring that out requires us to slowly move things from the realm of mere belief into the realm of certainty. You start by establishing a small toehold of certainty, where you are 100% sure that everything is doing what it is supposed to do. Then you expand that island of certainty, one small step at a time, until you find the point where your beliefs diverge from reality. At each step, the key idea is

When you have a belief about how your code works, try to find a simple, concrete check that can confirm or refute that belief.

  • Cat speaking

    One time when I had bug and nothing I was doing seemed to fix it, Prof. Melissa came over to help and the first thing she did was add std::cerr << "Hello, World\n"; at the top of main(). I was like, “What? Why would that help?”, and she muttered “I just wanted to be sure.” I guess she wanted to confirm that the source code she was looking at was actually the code that was running. It was, and the problem was somewhere else, but I guess she just wanted to be sure.

  • LHS Cow speaking

    That's a great example! Even the simplest checks can sometimes reveal surprising issues, like running the wrong version of the code. Always be willing to question your assumptions, even the ones that seem most basic.

  • Sheep speaking

    Like, what if I barely understand my own code to begin with?

  • LHS Cow speaking

    That's why we mention ideas such as talking through your code, with your partner, or even with a rubber duck. Explaining your code (including drawing memory diagrams) can help you understand it better, which, in turn, helps you debug it.

  • RHS Cow speaking

    Okay, let's see these techniques in action through a detailed example.

A CS 70 Grutoring Scenario

Imagine that you're a grutor in a future offering of CS 70, and a student pair comes to you with the following code that they wrote for a homework problem:

// Program to find average placement of the number 42 in random 0..99 array

#include <iostream>
#include <cs70/randuint32.hpp>
#include <vector>
#include <cassert>

// Find the index of the number 42 in a vector of uint32_t
size_t find42(const std::vector<uint32_t>& v) {
    size_t index = 0;
    // Search for the number 42 in the vector. We know it'll find it because
    // the array contains shuffled numbers between 0 and 99, inclusive.
    while (v[index] != 42) {
        ++index;
    }
    return index;
}

// This function fills a vector with 100 shuffled numbers between 0 and 99
void initArray(RandUInt32& rng, std::vector<uint32_t>& v) {
    v.clear();
    for (size_t i = 0; i < 100; ++i) {
        uint32_t num = rng.get(100);
        v.push_back(num);
    }
}

// Calculate statistics about the average position of 42 in the vector
int main() {
    RandUInt32 rng;
    std::vector<uint32_t> v;
    const size_t NUM_TRIALS = 10000;
    size_t totalIndex = 0;

    for (size_t trial = 0; trial < NUM_TRIALS; ++trial) {
        initArray(rng, v);
        size_t index = find42(v);
        totalIndex += index;
    }

    double averageIndex = static_cast<double>(totalIndex) / NUM_TRIALS;
    std::cout << "Average index of 42 over " << NUM_TRIALS
              << " trials: " << averageIndex << std::endl;

    return 0;
}

Look over the code briefly just so you have a broad sense of what it's doing.

  • Duck speaking

    Oooh, I think I see the bug already!

  • LHS Cow speaking

    That's not really the point here, and one reason we've cast the situation as you helping others see the bug. It's not about whether you can look at this code and see the bug, it's about the process of debugging.

The student pair tells you,

We wrote this code to find the number 42 in an array of integers, but it doesn't seem to work. Can you help us figure out what's wrong? We've figured out that if you ask it to find 0 instead of 42, it mostly works (although it doesn't seem like it's averaging at 50 like we expected). But if we put in 42, it segfaults.

Let's begin by looking at the evidence. When the code is hacked to find zero, it runs like this:

cs70 SERVER > clang++ -g -Wall -Wextra -o find42 find42.cpp -lranduint32
cs70 SERVER > ./find42
Average index of 42 over 10000 trials: 63.0418
cs70 SERVER > ./find42
Average index of 42 over 10000 trials: 62.6226

Unfortunately, when we try to find 42, we get

cs70 SERVER > clang++ -g -Wall -Wextra -o find42 find42.cpp -lranduint32
cs70 SERVER > ./find42
[1]    38787 segmentation fault (core dumped)  ./find42
cs70 SERVER > ./find42
[1]    38849 segmentation fault (core dumped)  ./find42

Improving the Evidence

While this evidence is suggestive, it doesn't really tell us much, and may actually lead us down the rabbit hole of wondering why it is that zero “works” and 42 doesn't. Whenever we have an abnormal program termination, such as a segmentation fault, assertion failure, or uncaught exception, we can quickly get much more informative evidence by running the program under valgrind:

cs70 SERVER > valgrind ./find42
==39893== Memcheck, a memory error detector
==39893== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==39893== Using Valgrind-3.22.0 and LibVEX; rerun with -h for copyright info
==39893== Command: ./find42
==39893==
==39893== Conditional jump or move depends on uninitialised value(s)
==39893==    at 0x109244: find42(std::vector<unsigned int, std::allocator<unsigned int> > const&) (find42.cpp:13)
==39893==    by 0x10931D: main (find42.cpp:37)
==39893==
==39893== Invalid read of size 4
==39893==    at 0x109241: find42(std::vector<unsigned int, std::allocator<unsigned int> > const&) (find42.cpp:13)
==39893==    by 0x10931D: main (find42.cpp:37)
==39893==  Address 0x4e39650 is 0 bytes after a block of size 512 alloc'd
==39893==    at 0x4846FA3: operator new(unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==39893==    by 0x109E45: std::__new_allocator<unsigned int>::allocate(unsigned long, void const*) (new_allocator.h:151)
==39893==    by 0x109B7F: allocate (alloc_traits.h:478)
==39893==    by 0x109B7F: std::_Vector_base<unsigned int, std::allocator<unsigned int> >::_M_allocate(unsigned long) (stl_vector.h:380)
==39893==    by 0x109889: void std::vector<unsigned int, std::allocator<unsigned int> >::_M_realloc_append<unsigned int const&>(unsigned int const&) (vector.tcc:596)
==39893==    by 0x1094E8: std::vector<unsigned int, std::allocator<unsigned int> >::push_back(unsigned int const&) (stl_vector.h:1294)
==39893==    by 0x1092A5: initArray(RandUInt32&, std::vector<unsigned int, std::allocator<unsigned int> >&) (find42.cpp:24)
==39893==    by 0x109312: main (find42.cpp:36)
==39893==
==39893==
==39893== Process terminating with default action of signal 11 (SIGSEGV)
==39893==  Access not within mapped region at address 0x5227000
==39893==    at 0x109241: find42(std::vector<unsigned int, std::allocator<unsigned int> > const&) (find42.cpp:13)
==39893==    by 0x10931D: main (find42.cpp:37)
==39893==  If you believe this happened as a result of a stack
==39893==  overflow in your program's main thread (unlikely but
==39893==  possible), you can try to increase the size of the
==39893==  main thread stack using the --main-stacksize= flag.
==39893==  The main thread stack size used in this run was 8388608.
==39893==
==39893== HEAP SUMMARY:
==39893==     in use at exit: 74,240 bytes in 2 blocks
==39893==   total heap usage: 9 allocs, 7 frees, 74,748 bytes allocated
==39893==
==39893== LEAK SUMMARY:
==39893==    definitely lost: 0 bytes in 0 blocks
==39893==    indirectly lost: 0 bytes in 0 blocks
==39893==      possibly lost: 0 bytes in 0 blocks
==39893==    still reachable: 74,240 bytes in 2 blocks
==39893==         suppressed: 0 bytes in 0 blocks
==39893== Rerun with --leak-check=full to see details of leaked memory
==39893==
==39893== Use --track-origins=yes to see where uninitialised values come from
==39893== For lists of detected and suppressed errors, rerun with: -s
==39893== ERROR SUMMARY: 1029769 errors from 2 contexts (suppressed: 0 from 0)
[1]    39893 segmentation fault (core dumped)  valgrind ./find42

It's easy to be a bit overwhelmed by the amount of output that valgrind produces, so here are some rules of thumb for reading it:

  • Look at the very first error it reports. In this case, it's a “Conditional jump or move depends on uninitialized value(s)”. That's the first moment that valgrind detected something wrong. Beneath that line is a backtrace that shows the sequence of function calls that led to the error. (We'll cover reading backtraces in a moment.)
  • Look at how the program exited—in this case, where it says “Process terminating with default action of signal 11 (SIGSEGV)”. This message tells us that the operating system killed the program because it tried to access memory that it wasn't allowed to access (a.k.a., a segfault). Again, there's a backtrace showing where this issue happened.

Interpreting Backtraces

One thing you'll notice is that backtraces are often cluttered with lots of functions that you didn't write; for example, std::__new_allocator<unsigned int>::allocate(unsigned long, void const*). These are functions from the C++ standard library that your code called, which called other functions, and so on. While it's possible that there's a bug in the library, it's much more likely that the issue is with the students' code, so the first thing to look for is the first function in the backtrace that they wrote. In this case, it's find42(std::vector<unsigned int, std::allocator<unsigned int> > const&), which is the function they wrote to find the number 42 in an array. Most importantly, several of the backtrace lines also say (find42.cpp:13) which tells us that the error occurred on line 13 of find42.cpp. That's a very strong hint that things went awry with the code on that line.

So we know that something went wrong when we were at the line that reads

    while (v[index] != 42) {
  • Horse speaking

    Hay! I think I've figured it out!

  • LHS Cow speaking

    That's good, but remember, we're looking at the debugging process, not just solving this bug. So let's imagine that the students are still mystified and keep going.

An Initial (Partial) Theory

An initial theory is that the array access is out-of-bounds. Adding an assert to check that the index is valid seems like a smart idea, so the loop now becomes

    while (v[index] != 42) {
        ++index;
        assert(index < v.size());
    }

Now, with this assertion in place, we don't need industrial-strength debugging tools like valgrind to catch the out-of-bounds error; we can just run the program normally:

cs70 SERVER > clang++ -g -Wall -Wextra -o find42 find42.cpp -lranduint32
cs70 SERVER > ./find42
find42: find42.cpp:15: size_t find42(const std::vector<uint32_t> &): Assertion `index < v.size()' failed.
[1]    40091 IOT instruction (core dumped)  ./find42
  • L-Diskie speaking

    IOT instruction is a very old-fashioned way of saying the program called the C-library abort() function to terminate itself, which is what assert does when the assertion fails. It actually stands for the PDP/11 IOT (input/output trap) instruction, which was historically used to implement system calls like abort().

The students now say,

Well, we could just use a bounded loop obviously, but the whole point of this function is that it's supposed to return the index of 42. There's no option for it to say, “I didn't find it”. Somehow 42 isn't there, which is crazy, because initArray writes all the numbers into the array.

The students claim expresses a belief, that initArray writes all the numbers into the array. But is that belief true? Let's apply the “expanding island of certainty” principle to this claim. One simple check is just checking whether the numbers do end up in the array.

So we change the loop to

    for (size_t i = 0; i < 100; ++i) {
        uint32_t num = rng.get(100);
        v.push_back(num);
        // Check that the number was actually added, two ways just to be sure
        assert(v.back() == num);
        assert(v[i] == num);
    }

Running the code now, we find that neither of these assertions ever fail. So the numbers are definitely getting added to the array. But is 42 one of them? We can check by adding a print statement:

        v.push_back(num);
        if (num == 42) {
            std::cerr << "- Added 42 at index " << i << "\n";
        }

Now we see

cs70 SERVER > clang++ -g -Wall -Wextra -o find42 find42.cpp -lranduint32
cs70 SERVER > ./find42
- Added 42 at index 8
- Added 42 at index 34
- Added 42 at index 50
find42: find42.cpp:15: size_t find42(const std::vector<uint32_t> &): Assertion `index < v.size()' failed.
[1]    40809 IOT instruction (core dumped)  ./find42
cs70 SERVER > ./find42
- Added 42 at index 57
- Added 42 at index 36
- Added 42 at index 52
- Added 42 at index 93
- Added 42 at index 42
- Added 42 at index 99
find42: find42.cpp:15: size_t find42(const std::vector<uint32_t> &): Assertion `index < v.size()' failed.
[1]    40893 IOT instruction (core dumped)  ./find42

Our student pair is even more perplexed now. They say,

Wow, it really is adding 42 to the array, but then when we try to find it, it still fails. This makes no sense!

You ask why it seems to be adding 42 multiple times, and they say,

Well, we're running multiple trials, and each time we run it, we reinitialize the array. So I guess it got more trials done on the second run for some reason.

So you add another print statement to show the trial number,

    for (size_t trial = 0; trial < NUM_TRIALS; ++trial) {
        std::cerr << "Trial " << trial << ":\n";
        initArray(rng, v);
        size_t index = find42(v);
        totalIndex += index;
    }

and when run it produces

cs70 SERVER > ./find42
Trial 0:
- Added 42 at index 20
Trial 1:
- Added 42 at index 95
Trial 2:
find42: find42.cpp:15: size_t find42(const std::vector<uint32_t> &): Assertion `index < v.size()' failed.
[1]    41567 IOT instruction (core dumped)  ./find42

The students now say,

See! That's what we said! Multiple trials. We put the numbers in but sometimes it still can't find 42. This is so weird.

Gathering More Evidence

Sometimes we need more insight to see what's going on. One question we might consider is, “If 42 isn't in the array, what is in the array?” Let's find out by adding a print statement to dump the entire contents of the array:

    std::cerr << "Array contents: ";
    for (size_t i = 0; i < v.size(); ++i) {
        std::cerr << v[i] << " ";
    }
    std::cerr << "\n";

Now we see:

cs70 SERVER > clang++ -g -Wall -Wextra -o find42 find42.cpp -lranduint32
cs70 SERVER > ./find42
Trial 0:
- Added 42 at index 45
- Array contents: 56 51 60 64 99 37 61 30 95 76 25 40 10 24 29 80 14 9 6 10 75 30 47 82 4 20 78 80 30 71 12 52 59 7 10 37 17 43 11 32 19 34 38 62 79 42 76 45 75 65 12 45 52 58 8 35 93 13 71 37 33 43 97 79 64 61 49 0 9 95 51 3 27 37 14 91 84 9 19 3 55 69 90 58 45 66 39 35 11 21 48 6 68 28 49 83 38 52 41 83
Trial 1:
- Array contents: 74 28 21 86 96 36 68 38 76 90 14 72 19 22 58 99 93 37 71 33 55 52 75 14 10 36 5 62 67 17 36 58 31 52 24 90 8 68 9 52 48 74 70 88 51 4 54 92 82 99 24 7 90 60 45 6 64 58 71 86 31 19 84 67 64 81 66 87 44 52 86 47 45 45 40 83 17 27 41 94 17 58 92 74 51 24 70 80 62 5 17 40 0 22 37 88 94 98 98 90
find42: find42.cpp:15: size_t find42(const std::vector<uint32_t> &): Assertion `index < v.size()' failed.
[1]    41898 IOT instruction (core dumped)  ./find42

The students look at this output and say,

See, filled with numbers, but no 42 in trial 1. It's got to be there. Wait, why is 51 in there twice in trial 0? That's weird. Oh wait… I— I think I see the problem. Nothing stops there from being repeats, and if some numbers are repeated, then there won't be a space for some others. We have to avoid duplicates.

In just a few moments, a misconception about what it meant to put the “shuffled numbers from 0 to 99” into the array collapses. The students even realize that the assignment mentioned std::shuffle, which is a big hint that they should have been shuffling a list of the numbers 0 to 99, not generating random numbers with replacement. At this point, their original code seems absurdly ill-conceived. They look sheepish and apologetic, saying,

Sorry! That was really dumb of us. We should have realized that sooner. Thanks for helping us figure it out!

And, as a grutor, this is your moment for camaraderie and reassurance, because that experience is everyone's experience in every debugging session ever. Once you understand things properly, it becomes hard to see how you could have been confused.

Key Debugging Principles

The example above illustrates several important principles:

Start with the Evidence You Have
Even if it's not very informative (like a bare segfault), it's your starting point.
Improve Your Evidence
Run with Valgrind, add assertions, add print statements—gather better information about what's actually happening.
Check Your Assumptions Systematically
When the students said “initArray writes all the numbers into the array,” they were making an assumption. Testing that assumption revealed the misconception.
The “Expanding Island of Certainty“ Approach
Start from a point where things work as you expect, then creep forward, checking assumptions until you find where reality diverges from your mental model.
The Bug Will Make Sense in Hindsight
Once you understand what's really happening, the bug often seems obvious. That's not because you were foolish—it's because you've learned something new about how your code actually behaves.
Everyone's Experience Is Like This
Every programmer—from beginners to experts—goes through this same process of discovery, confusion, and eventual understanding. The main difference is that experienced programmers have seen more patterns and can sometimes recognize issues faster.
  • Cat speaking

    I always feel embarrassed when the bug turns out to be something simple…

  • LHS Cow speaking

    Everyone does! But that “obvious in hindsight” feeling just means you learned something. You're not supposed to magically know everything before you debug—that's what debugging is for.

When to Ask for Help

Another thing to keep in mind is that you don't have to solve every bug on your own!1 Computer science is a collaborative field, and getting unstuck is an important part of learning.

Good times to ask for help include:

  • You've tried the techniques above and are still stuck.
  • You've been working on the same bug for more than 30-45 minutes without progress.
  • You've found the problem but don't understand why it's a problem.
  • You think you might be dealing with undefined behavior and want a second opinion.

When you ask for help (on Piazza, in grutoring hours, or in office hours), be ready to show them what you've already tried and the key pieces of evidence you've gathered. Knowing that information helps others understand where you're stuck and gives them better context for helping you. Not only that, but the process of showing what you've already tried often helps you see the problem more clearly—explaining what you've tried is a form of rubber-duck debugging!

  • Hedgehog speaking

    What if I'm embarrassed about not being able to figure it out for myself?

  • LHS Cow speaking

    Try not to be—everyone gets stuck sometimes. The grutors and instructors are there specifically to help you with these issues. They've all been through the same frustrations you're experiencing, and they want to help you get unstuck.

  • RHS Cow speaking

    Don't forget that helping someone else debug their code is also an opportunity to learn—everyone wins!

Summary

Debugging is fundamentally about discovering your own misconceptions.

  • Use the tools at your disposal: print statements, Valgrind, assertions, compiler warnings.
  • Improve your evidence systematically until you can see what's really happening.
  • Check your assumptions—the bug is hiding in something you're taking for granted.
  • Expand your “island of certainty” from what you know works toward what doesn't.
  • Ask for help when you're stuck—collaboration is a strength, not a weakness.

With practice, you'll get better at recognizing patterns, asking the right questions, and tracking down bugs efficiently. Every debugging session, as frustrating as it may be in the moment, is making you a better programmer.

  • Pig speaking

    Are there MORE POWERFUL debugging techniques than these?

  • LHS Cow speaking

    As far as an overall approach goes, this page has covered the main ideas. But there is one more tool we haven't mentioned yet: using a source-level debugger. Debuggers let you run your program step-by-step, inspect variables, and see exactly what's happening at each line of code. They can be very powerful, especially for complex bugs where interactive investigation is helpful, but they also have a steeper learning curve and aren't always faster than traditional methods.

  • RHS Cow speaking

    On our server, we have gdb (the GNU Debugger) installed, which is a popular source-level debugger for C, C++, and other languages. The Using GDB guide provides an introduction to using gdb effectively if you want to give it a try.


  1. At least for CS 70. If you're working as a programmer, you probably have other coders on your team or on other projects you can ask for help. Even if you're working completely alone, there will be resources available to you, from search engines, to sites like Stack Exchange, to AI assistants. Here in CS 70 we have rules about what resources you can use, but you also have the profs, the grutors, and your peers backing you up!] 

(When logged in, completion status appears here.)