Debugging

Your code will have bugs. Everyone’s code has bugs. The difference between beginners and experienced programmers isn’t that experts write perfect code - it’s that they know how to find and fix problems quickly.

This lesson teaches you the art of debugging - tracking down what’s wrong and making it right.

Two Kinds of Errors

Before your program even runs, you might hit two different kinds of problems:

Compiler errors happen when you try to build your program. The compiler can’t understand your code, so it refuses to create an executable. You have to fix these before you can run anything.

Runtime errors happen while your program is running. The code compiled fine, but something goes wrong when you actually execute it. Maybe it crashes, maybe it gives wrong answers, maybe it runs forever.

Learning to tell these apart is the first step in debugging.

Reading Compiler Errors

Compiler errors look scary at first. Here’s a typical one:

main.c:7:12: error: expected ';' before 'return'
    7 |     printf("Hello")
      |            ^
    8 |     return 0;
      |     ~~~~~~

Let’s break this down:

  • main.c - the file with the problem
  • 7 - the line number
  • 12 - the column (how far across the line)
  • error: - this is an error (as opposed to a warning)
  • expected ';' before 'return' - what went wrong

The compiler is telling you: “On line 7, around column 12, I expected a semicolon but found something else.”

Look at line 7 - the printf statement is missing its semicolon. Add it and the error goes away.

Common Compiler Errors and What They Mean

“undeclared identifier” or “use of undeclared identifier”

error: 'countr' undeclared

You’re using a variable or function that doesn’t exist. Usually a typo - you probably meant counter.

“expected ‘;’ before…”

error: expected ';' before 'int'

The line above is missing a semicolon.

“implicit declaration of function”

warning: implicit declaration of function 'prntf'

You’re calling a function the compiler doesn’t know about. Either you misspelled it (prntf vs printf) or you forgot to #include the right header.

“conflicting types”

error: conflicting types for 'calculate'

You declared a function one way but defined it differently. Maybe the return types don’t match, or the parameters are different.

“expected ‘)’ before…”

error: expected ')' before ';' token

Mismatched parentheses. Count your opening and closing parens.

“assignment to expression with array type”

error: assignment to expression with array type

You tried to assign to an array directly. Arrays can’t be reassigned - you need to copy elements one by one or use strcpy for strings.

The First Error Matters Most

When the compiler spits out 50 errors, don’t panic. Fix the first one and recompile. Often a single mistake (like a missing brace) confuses the compiler so much that it reports dozens of fake errors afterward.

Fix errors from the top down, recompiling after each fix.

Printf Debugging

The simplest debugging technique is also the most useful: print stuff out.

#include <stdio.h>

int calculate_total(int items[], int count) {
    int total = 0;
    printf("DEBUG: Starting calculation, count = %d\n", count);

    for (int i = 0; i <= count; i++) {  // Bug: should be i < count
        printf("DEBUG: i = %d, items[i] = %d\n", i, items[i]);
        total += items[i];
        printf("DEBUG: Running total = %d\n", total);
    }

    printf("DEBUG: Final total = %d\n", total);
    return total;
}

By printing values at each step, you can see exactly what’s happening. When the output doesn’t match what you expect, you’ve found your bug.

Printf Debugging Tips

Print variable values, not just “got here”:

// Not very helpful
printf("checkpoint 1\n");

// Much more helpful
printf("checkpoint 1: x=%d, y=%d, result=%d\n", x, y, result);

Add labels so you know which print is which:

printf("[calculate_total] Starting with count=%d\n", count);
printf("[calculate_total] Loop iteration i=%d\n", i);

Print before AND after suspicious code:

printf("Before mystery_function: x = %d\n", x);
mystery_function(&x);
printf("After mystery_function: x = %d\n", x);

Use a DEBUG flag to turn prints on and off:

#define DEBUG 1

#if DEBUG
    printf("Debug info: x = %d\n", x);
#endif

Set DEBUG to 0 when you don’t want the output cluttering things up.

Using assert()

The assert() function is a powerful debugging tool. It checks if a condition is true. If it’s not, the program stops immediately and tells you what went wrong.

#include <stdio.h>
#include <assert.h>

int divide(int a, int b) {
    assert(b != 0);  // Program stops here if b is zero
    return a / b;
}

int main(void) {
    printf("%d\n", divide(10, 2));  // Works fine
    printf("%d\n", divide(10, 0));  // Crashes with assertion failure
    return 0;
}

When the assertion fails, you get a message like:

Assertion failed: b != 0, file main.c, line 5

When to Use assert()

Use assertions to check things that should never happen:

// Array index should always be valid
assert(index >= 0 && index < array_size);

// Pointer should never be NULL after allocation
int *data = malloc(100 * sizeof(int));
assert(data != NULL);

// Function should only receive positive values
void process_positive(int value) {
    assert(value > 0);
    // ... rest of function
}

Assertions are documentation that enforces itself. They say “this must be true” and prove it by crashing if it’s not.

assert() vs Error Handling

Don’t use assertions for things that might legitimately go wrong in normal use:

// BAD: User input could be anything
assert(user_input > 0);

// GOOD: Check and handle gracefully
if (user_input <= 0) {
    printf("Please enter a positive number\n");
    return -1;
}

Assertions are for catching programmer mistakes during development. Error handling is for dealing with real-world problems in production.

Introduction to GDB

Printf debugging works, but for serious bugs, you want a real debugger. GDB (GNU Debugger) lets you pause your program, look at variables, and step through code line by line.

Compiling for Debugging

First, compile with the -g flag. This adds debugging information to your program:

gcc -g -o myprogram myprogram.c

Without -g, the debugger won’t know how your machine code relates to your source code.

Starting GDB

Run GDB with your program:

gdb ./myprogram

You’ll see a (gdb) prompt. Your program isn’t running yet - you’re just inside the debugger.

Essential GDB Commands

run (or just r) - Start your program:

(gdb) run

break (or b) - Set a breakpoint. The program pauses when it reaches this line:

(gdb) break main
(gdb) break 15
(gdb) break calculate_total

You can break at a function name or a line number.

next (or n) - Execute the next line, stepping over function calls:

(gdb) next

step (or s) - Execute the next line, stepping into function calls:

(gdb) step

The difference: if the line calls a function, next runs the whole function, while step goes inside it.

print (or p) - Show the value of a variable:

(gdb) print x
(gdb) print array[5]
(gdb) print *pointer

backtrace (or bt) - Show how you got here (the call stack):

(gdb) backtrace

This shows which function called which. Essential when a crash happens deep in your code.

continue (or c) - Keep running until the next breakpoint:

(gdb) continue

quit (or q) - Exit GDB:

(gdb) quit

A GDB Session Example

Say you have a buggy program:

#include <stdio.h>

int sum_array(int arr[], int size) {
    int total = 0;
    for (int i = 0; i <= size; i++) {  // Bug: should be <
        total += arr[i];
    }
    return total;
}

int main(void) {
    int numbers[] = {10, 20, 30};
    int result = sum_array(numbers, 3);
    printf("Sum: %d\n", result);
    return 0;
}

Debug it like this:

$ gcc -g -o buggy buggy.c
$ gdb ./buggy
(gdb) break sum_array
Breakpoint 1 at 0x1234: file buggy.c, line 4.
(gdb) run
Breakpoint 1, sum_array (arr=0x7fff..., size=3) at buggy.c:4
4           int total = 0;
(gdb) next
5           for (int i = 0; i <= size; i++) {
(gdb) next
6               total += arr[i];
(gdb) print i
$1 = 0
(gdb) print arr[i]
$2 = 10
(gdb) continue
... (let it run, see what happens)

By stepping through and checking values, you can watch the bug happen in slow motion.

Common Runtime Bugs

Some bugs only show up when you run the program. Here are the classics:

Segmentation Faults (Segfaults)

A segfault means your program tried to access memory it shouldn’t. Common causes:

Dereferencing NULL:

int *ptr = NULL;
*ptr = 5;  // CRASH

Array out of bounds:

int arr[10];
arr[100] = 5;  // CRASH (or worse - silent corruption)

Using freed memory:

int *data = malloc(sizeof(int));
free(data);
*data = 5;  // CRASH

Stack overflow from infinite recursion:

void infinite(void) {
    infinite();  // Eventually crashes
}

When you get a segfault, run your program in GDB. It will stop at the crash and show you where it happened. Use backtrace to see how you got there.

Infinite Loops

Your program runs forever and never produces output:

int i = 0;
while (i < 10) {
    printf("%d\n", i);
    // Forgot to increment i!
}

When this happens, press Ctrl+C to stop the program. Add printf statements inside the loop to see what’s happening. Check that your loop condition can actually become false.

Off-by-One Errors

One of the most common bugs in programming. You’re almost right, but you’re one off:

// Bug: processes 11 elements instead of 10
for (int i = 0; i <= 10; i++) { ... }

// Bug: misses the last element
for (int i = 0; i < size - 1; i++) { ... }

// Bug: reads one past the end of the string
char str[10];
for (int i = 0; i <= strlen(str); i++) { ... }

The fix is usually changing <= to < or vice versa, or adjusting the start/end values.

Remember: arrays are zero-indexed. An array of size 10 has valid indices 0 through 9, not 1 through 10.

Uninitialized Variables

Local variables in C start with garbage values:

int count;  // Could be anything!
count++;    // Adding 1 to garbage

Always initialize your variables:

int count = 0;

The compiler might warn you about this. Pay attention to warnings.

Debugging Strategies

Knowing the tools is one thing. Knowing how to think about bugs is another.

Binary Search for Bugs

If your program is large and something is wrong, don’t read through the whole thing. Use binary search:

  1. Find a point roughly in the middle of your code
  2. Add a print statement there
  3. Run the program
  4. Is the output correct at that point?
    • Yes: The bug is in the second half
    • No: The bug is in the first half
  5. Repeat, narrowing down each time

This finds bugs in log(n) time instead of n time. In a 1000-line program, you can find the problem in about 10 steps.

Rubber Duck Debugging

This sounds silly but it works. Explain your code, line by line, to a rubber duck (or any inanimate object). Explain what each line does and why.

The act of explaining forces you to think carefully. You’ll often say “and this line does… wait, that’s not right.”

You don’t need an actual duck. A coffee mug works. Or just explain it out loud to yourself. The key is verbalizing your assumptions.

Simplify the Problem

Can’t figure out what’s wrong? Make a smaller version of the problem:

  1. Copy your buggy function to a new file
  2. Write a simple main() that just calls that function
  3. Hardcode the inputs that cause the bug
  4. Remove everything that isn’t needed to reproduce the bug

Now you have a tiny program that shows the bug. This is much easier to debug than a 5000-line project.

Check Your Assumptions

When you’re stuck, write down what you think is true:

  • “The array has 10 elements”
  • “The pointer is not NULL here”
  • “The loop runs exactly 5 times”

Then verify each assumption with print statements or the debugger. Often you’ll find one of your assumptions is wrong.

Read the Error Message Again

This sounds obvious, but do it anyway. Read the error message slowly, word by word. Beginners often glance at errors and assume they understand. The message usually tells you exactly what’s wrong - you just have to actually read it.

Try It Yourself

  1. Create a program with a deliberate off-by-one error in a loop. Use printf debugging to find exactly where it goes wrong.

  2. Write a function that crashes if given a NULL pointer. Use assert() to make the crash obvious. Then use GDB to see the backtrace when the assertion fails.

  3. This code has a bug. Find it using any debugging technique:

    int find_max(int arr[], int size) {
        int max = 0;
        for (int i = 0; i < size; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
        }
        return max;
    }

    (Hint: What if all numbers are negative?)

  4. Practice GDB: Write a simple program with a few functions that call each other. Set breakpoints, step through, and use print to examine variables. Get comfortable with the commands.

  5. Take some old code you wrote that has a bug you never fixed. Apply the debugging strategies from this lesson to finally track it down.

Common Mistakes

  • Ignoring warnings. Compiler warnings often point directly at bugs. Compile with -Wall to see all warnings, and fix them.

  • Debugging with stale code. Make sure you recompile after making changes. Many frustrated debugging sessions happen because someone forgot to rebuild.

  • Not checking return values. Functions like malloc, fopen, and scanf tell you when they fail. If you ignore their return values, bugs become invisible.

  • Assuming you know where the bug is. You think you know, but you might be wrong. Verify before you spend an hour staring at the wrong function.

  • Making multiple changes at once. Change one thing, test. Change another thing, test. If you change five things and it starts working, you don’t know which change fixed it (or if you introduced new bugs).

  • Not using version control. Git lets you go back to when things worked. If you break something badly, you can revert. Without version control, you might lose hours of work.

The Debugging Mindset

Debugging is detective work. Something went wrong. You have clues (error messages, wrong output, crashes). Your job is to follow the evidence to the culprit.

Stay calm. Stay methodical. Don’t just randomly change things and hope for the best. Form a hypothesis, test it, and repeat.

The best debuggers aren’t the ones who never have bugs. They’re the ones who systematically track bugs down and squash them, every single time.

Next Up

In Part 28, we’ll put everything together in a complete practical project that uses all the skills you’ve learned.


Enjoyed This?

If this helped something click, subscribe to my YouTube channel. More content like this, same approach - making things stick without insulting your intelligence. It’s free, it helps more people find this stuff, and it tells me what’s worth making more of.