Day 13: Error Handling & Debugging#

Overview#

Professional programs handle errors gracefully and can be debugged effectively. Today we’ll learn common runtime errors, debugging techniques using VSCode, and error handling best practices.

What We’ll Learn Today#

  • Common C runtime errors
  • Error prevention strategies
  • Using assert() for debugging
  • Using printf() debugging
  • VSCode debugging features
  • Setting breakpoints
  • Inspecting variables
  • Step-by-step execution

Common Runtime Errors#

1. Segmentation Fault (Segfault)#

Accessing invalid memory:

// ❌ Dereferencing NULL pointer
int* ptr = NULL;
printf("%d\n", *ptr);  // Segfault!

// ❌ Accessing out of bounds
int arr[5];
arr[10] = 20;  // Segfault!

// ❌ Using uninitialized pointer
int* ptr;
*ptr = 10;  // Segfault!

Prevention:

// ✅ Check for NULL
if (ptr != NULL) {
    printf("%d\n", *ptr);
}

// ✅ Check array bounds
if (index >= 0 && index < size) {
    arr[index] = value;
}

// ✅ Initialize pointers
int x = 10;
int* ptr = &x;

2. Stack Overflow#

Too many recursive calls or large local arrays:

// ❌ Infinite recursion
void infinite() {
    infinite();  // Stack overflow!
}

// ❌ Large array on stack
void function() {
    int huge_array[1000000];  // Stack overflow!
}

Prevention:

// ✅ Base case in recursion
void recursive(int n) {
    if (n <= 0) return;
    recursive(n - 1);
}

// ✅ Use dynamic allocation
int* arr = malloc(1000000 * sizeof(int));

3. Buffer Overflow#

Writing beyond array bounds:

// ❌ No bounds checking
char buffer[10];
strcpy(buffer, "This is a very long string");  // Buffer overflow!

// ✅ Safe version
char buffer[10];
strncpy(buffer, "This is a very long string", 9);
buffer[9] = '\0';

4. Memory Leaks#

Allocating memory without freeing:

// ❌ Memory leak
void function() {
    int* ptr = malloc(100 * sizeof(int));
    // ... use ptr ...
    return;  // Never freed!
}

// ✅ Proper cleanup
void function() {
    int* ptr = malloc(100 * sizeof(int));
    // ... use ptr ...
    free(ptr);
}

5. Division by Zero#

// ❌ Division by zero
int result = 10 / 0;  // Runtime error!

// ✅ Check before dividing
if (divisor != 0) {
    result = 10 / divisor;
} else {
    printf("Cannot divide by zero\n");
}

6. Use-After-Free#

Using pointer after freeing:

// ❌ Use after free
int* ptr = malloc(10 * sizeof(int));
free(ptr);
printf("%d\n", *ptr);  // Undefined behavior!

// ✅ Set to NULL after free
int* ptr = malloc(10 * sizeof(int));
free(ptr);
ptr = NULL;
if (ptr != NULL) {
    printf("%d\n", *ptr);
}

Error Handling Strategies#

1. Return Error Codes#

int divide(int a, int b, int* result) {
    if (b == 0) {
        return -1;  // Error code
    }
    *result = a / b;
    return 0;  // Success
}

int main() {
    int result;
    if (divide(10, 0, &result) != 0) {
        printf("Error: Division by zero\n");
        return 1;
    }
    printf("Result: %d\n", result);
    return 0;
}

2. Use assert() for Debugging#

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

int main() {
    int x = -5;

    // This assertion will fail if x < 0
    assert(x >= 0);  // Program terminates here

    printf("x is valid: %d\n", x);

    return 0;
}

When to use:

  • Check assumptions during development
  • Disabled in release builds
  • Only for logic errors, not user input errors

3. Defensive Programming#

#include <stdio.h>
#include <stdlib.h>

int* createArray(int size) {
    // Check for invalid size
    if (size <= 0) {
        printf("Error: Size must be positive\n");
        return NULL;
    }

    // Check for allocation failure
    int* arr = malloc(size * sizeof(int));
    if (arr == NULL) {
        printf("Error: Memory allocation failed\n");
        return NULL;
    }

    return arr;
}

int main() {
    int* arr = createArray(10);

    if (arr == NULL) {
        printf("Failed to create array\n");
        return 1;
    }

    free(arr);
    return 0;
}

Debugging Techniques#

Technique 1: Print Statements#

#include <stdio.h>

int factorial(int n) {
    printf("DEBUG: factorial called with n = %d\n", n);

    if (n <= 1) {
        printf("DEBUG: base case reached\n");
        return 1;
    }

    int result = n * factorial(n - 1);
    printf("DEBUG: factorial(%d) = %d\n", n, result);
    return result;
}

int main() {
    printf("DEBUG: starting program\n");
    int result = factorial(5);
    printf("Result: %d\n", result);
    return 0;
}

Technique 2: Logging with Levels#

#include <stdio.h>

#define DEBUG 1
#define INFO 2
#define ERROR 3

void log_message(int level, const char* message) {
    const char* level_name[] = {"DEBUG", "INFO", "ERROR"};
    if (level >= DEBUG) {  // Only print if level is high enough
        printf("[%s] %s\n", level_name[level - 1], message);
    }
}

int main() {
    log_message(DEBUG, "Program started");
    log_message(INFO, "Processing data");
    log_message(ERROR, "Something went wrong");

    return 0;
}

VSCode Debugging#

Step 1: Install C/C++ Extension#

Already done! (From Day 1)

Step 2: Create launch.json#

VSCode automatically creates this when you run Debug.

Step 3: Set Breakpoints#

  1. Click on the left margin next to a line number
  2. A red dot appears - that’s a breakpoint
  3. When the program reaches that line, it pauses

Step 4: Start Debugging#

  1. Press F5 or go to Run → Start Debugging
  2. Choose C++ environment
  3. Program pauses at breakpoints

Step 5: Inspect Variables#

When paused at a breakpoint:

  1. Variables Panel (left sidebar) shows all variables
  2. Watch Panel - add specific variables to monitor
  3. Hover over variables in code to see current values

Step 6: Step Through Code#

  • Step Over (F10): Execute current line, don’t go into functions
  • Step Into (F11): Go into function calls
  • Step Out (Shift+F11): Execute rest of function, return here
  • Continue (F5): Resume execution until next breakpoint

Debugging Example Program#

Program with Bug#

#include <stdio.h>

int calculateSum(int arr[], int size) {
    int sum = 0;
    for (int i = 0; i <= size; i++) {  // BUG: <= should be <
        sum += arr[i];
    }
    return sum;
}

int main() {
    int numbers[] = {10, 20, 30, 40, 50};
    int total = calculateSum(numbers, 5);
    printf("Sum: %d\n", total);

    return 0;
}

Debugging Steps#

  1. Set breakpoint on the for loop line
  2. Press F5 to start debugging
  3. Step Into the function (F11)
  4. Watch the variable i as it changes
  5. Notice when i reaches 5 (invalid index)
  6. Step Over to see the invalid access
  7. Fix the condition to i < size

Using gdb (Command Line Debugger)#

For advanced debugging:

# Compile with debugging symbols
gcc -g program.c -o program

# Start gdb
gdb ./program

# Common gdb commands
(gdb) break main              # Set breakpoint
(gdb) run                     # Start program
(gdb) next                    # Step over
(gdb) step                    # Step into
(gdb) print variable_name     # Print variable
(gdb) continue                # Resume
(gdb) quit                    # Exit gdb

Practical Debugging Example#

Buggy Program#

#include <stdio.h>

int findMax(int arr[], int size) {
    int max = arr[0];

    for (int i = 1; i < size; i++) {
        if (arr[i] > max) {
            max = arr[i];
        }
    }

    return max;
}

int main() {
    int scores[] = {85, 92, 78, 95, 88};

    int result = findMax(scores, 5);

    printf("Maximum score: %d\n", result);

    return 0;
}

Set Breakpoints and Debug#

  1. Set breakpoint at for loop
  2. Set breakpoint at if statement
  3. Watch how max changes
  4. Verify logic is correct

Checklist for Error Prevention#

  • Initialize all variables before use
  • Check all pointer operations for NULL
  • Check array bounds before access
  • Check divisor before division
  • Free all dynamically allocated memory
  • Close all open files
  • Handle function return values
  • Validate user input
  • Use assertions for impossible conditions
  • Add meaningful error messages

Practice Exercises#

Exercise 1: Fix the Bug#

Find and fix bugs in provided programs.

Exercise 2: Defensive Coding#

Rewrite a program with proper error handling.

Exercise 3: Debug Session#

Use VSCode debugger to:

  • Set multiple breakpoints
  • Watch variable changes
  • Identify and fix a logic error

Summary#

✅ Understood common runtime errors
✅ Implemented error checking
✅ Used print-based debugging
✅ Configured VSCode debugging
✅ Set and used breakpoints
✅ Stepped through code
✅ Inspected variables
✅ Applied defensive programming

Key Points to Remember#

  1. Check before use: NULL pointers, array bounds, divisors
  2. Always free memory that you allocate
  3. Always close files you open
  4. Validate input from users
  5. Use assertions during development
  6. Use breakpoints for complex bugs
  7. Step through code methodically
  8. Check return values of functions

Next Steps#

Tomorrow is the final day! We’ll build a complete mini-project - a Student Record Management System!

→ Continue to Day 14

← Back to Day 12