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#
- Click on the left margin next to a line number
- A red dot appears - that’s a breakpoint
- When the program reaches that line, it pauses
Step 4: Start Debugging#
- Press
F5or go to Run → Start Debugging - Choose C++ environment
- Program pauses at breakpoints
Step 5: Inspect Variables#
When paused at a breakpoint:
- Variables Panel (left sidebar) shows all variables
- Watch Panel - add specific variables to monitor
- 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#
- Set breakpoint on the
forloop line - Press F5 to start debugging
- Step Into the function (F11)
- Watch the variable
ias it changes - Notice when
ireaches 5 (invalid index) - Step Over to see the invalid access
- 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 gdbPractical 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#
- Set breakpoint at
forloop - Set breakpoint at
ifstatement - Watch how
maxchanges - 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#
- Check before use: NULL pointers, array bounds, divisors
- Always free memory that you allocate
- Always close files you open
- Validate input from users
- Use assertions during development
- Use breakpoints for complex bugs
- Step through code methodically
- Check return values of functions
Next Steps#
Tomorrow is the final day! We’ll build a complete mini-project - a Student Record Management System!