Learn C Language - Error Handling & Debugging
Episode 11 of 24

Learn C Language - Error Handling & Debugging

This episode discusses how to handle and find bugs: error propagation using return codes and errno, diagnostics with assert and logging, debugging sessions with GDB and LLDB using breakpoints, backtraces, and watchpoints, as well as the static analysis tools clang-tidy, cppcheck, and sanitizers.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

A correct program is merely one whose bugs haven't been found yet. Episode 11 covers two sides of reliability: how programs report errors cleanly, and how programmers find the root cause with the right tools.

C has no exceptions like modern languages. Errors are propagated through return values, the errno variable, and exit statuses. You need to pick one strategy and stay consistent so the error flow can be followed.

To find bugs, C relies on a layered arsenal: assert and logging for early detection, GDB and LLDB for interactive inspection, and then clang-tidy, cppcheck, and sanitizers to catch mistakes invisible to the eye.

Error Propagation with Return Codes and errno

Designing Return Values

The most common strategy: a function returns a status code, where 0 means success and a non-zero value means error. The actual result is handed over through a pointer parameter:

Propagating errors via return codes
#include <stdio.h>
 
int bagi(int a, int b, int *hasil) {
    if (b == 0) {
        return -1;
    }
    *hasil = a / b;
    return 0;
}
 
int main(void) {
    int hasil;
    if (bagi(10, 0, &hasil) != 0) {
        fprintf(stderr, "gagal membagi\n");
        return 1;
    }
    printf("%d\n", hasil);
    return 0;
}

The call bagi(10, 0, &hasil) returns -1 when the divisor is zero and writes the result through the pointer on success. The caller checks the return value before using hasil. This pattern demands discipline: every caller must check the status.

errno and Exit Status

For system errors, errno stores the failure code and perror prints it. On the process side, main's return value becomes the program's exit status, which the shell can inspect. Consistency: always choose one strategy per module and document its contract so callers are never confused.

Diagnostics with assert and Logging

Validating Assumptions with assert

The assert macro from <assert.h> checks a condition during debugging. If it fails, the program stops and prints the location:

Validating with assert
#include <assert.h>
 
int kuadrat(int x) {
    return x * x;
}
 
int main(void) {
    assert(kuadrat(4) == 16);
    assert(kuadrat(-3) == 9);
    return 0;
}

assert(kuadrat(4) == 16) validates an assumption that must always hold. When NDEBUG is defined, all asserts are removed from the binary. Use assert for conditions that truly must never happen, not for validating user input.

Logging for a Runtime Trail

Logging records what happens while the program runs. With fprintf(stderr, ...) for error messages and log levels for debugging, you can trace the sequence of events. In production, logs act as a witness — a topic that will expand into observability in episode 22.

Debugging with GDB and LLDB

Breakpoints and Stepping Through Code

GDB is the GNU debugger that works with executables compiled using -g. Start a session and set a breakpoint:

Interactive GDB session
gcc -g -Wall program.c -o program
gdb -q ./program

Inside the (gdb) prompt, you type commands. break main sets a breakpoint at the start of the main function, run runs the program until it stops, and next executes one line without stepping into called functions. To step into a function, use step.

Backtrace and Watchpoint

When the program stops, bt prints a backtrace — the chain of function calls that led execution to this point. A backtrace answers the most important question on a crash: where this function was called from.

watch sets a watchpoint that stops when a variable's value changes:

Important GDB commands
(gdb) break main
(gdb) run
(gdb) watch variabel
(gdb) continue
(gdb) bt
(gdb) print variabel

The watch variabel above triggers a stop every time the value changes, useful for finding who corrupted the data. print variabel inspects the current contents of a variable. LLDB is a modern alternative with similar commands: breakpoint set, run, and bt.

Static Analysis Tools

clang-tidy and cppcheck

Static analysis finds bugs without running the program. clang-tidy checks conventions and potential problems, while cppcheck focuses on real bugs such as null dereferences and use-after-free:

Static analysis
clang-tidy program.c -- -Iinclude
cppcheck --enable=all program.c

The command cppcheck --enable=all program.c analyzes the file without compiling. Run both on every change — these tools find bugs that slip past human eyes and even the compiler.

Sanitizers: Runtime Detection

Sanitizers embed checks into the binary at compile time. AddressSanitizer detects out-of-bounds and use-after-free, UndefinedBehaviorSanitizer detects undefined behavior:

Build with sanitizers
gcc -g -fsanitize=address,undefined program.c -o program
./program

The flag -fsanitize=address,undefined links in a runtime that checks memory on every access. When an error occurs, the program stops with a detailed report of the bug's location. Sanitizers add overhead, so enable them during development and turn them off in production builds.

Tip

The recommended strategy order: compile with -Wall -Wextra, run clang-tidy and cppcheck, build with sanitizers, and only then use GDB for the remaining cases. Each layer catches a different class of bugs.

Closing

Key takeaways:

  • Errors are propagated through return codes, errno, and exit statuses.
  • assert validates internal assumptions; logging records a runtime trail.
  • GDB uses breakpoints, step, watchpoints, and backtraces.
  • LLDB is a modern alternative with similar concepts.
  • clang-tidy and cppcheck find bugs without running the program.
  • Sanitizers catch out-of-bounds and undefined behavior at runtime.

In the next episode 12 we will discuss networking and sockets — the basics of TCP and UDP socket programming, the socket API such as socket, bind, listen, accept, connect, send, and recv, a simple client-server with message exchange, up to network byte order, socket addresses, and error handling.

Learn C Language - Error Handling & Debugging | Learn C Language