Skip to Content
Course content

121: Debugging Assertions with assert.h

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Look, we've all been there. You're staring at a segfault that only happens once every thousand runs, and you spend three hours stepping through a debugger only to realize that a pointer was NULL when it absolutely should not have been. Usually, our instinct is to "protect" the code. We write an if statement to check the pointer, print an error message, and then return early to avoid the crash. It feels like you're making the code more robust, but in reality, you're just hiding the crime scene.

Let's look at a real-world scenario. Imagine you're writing a physics engine for a game, and you have a function that applies a force to a rigid body. In your head, the body pointer must always be valid because the calling function is responsible for managing the object's lifecycle.

The trap of the silent return

Here is how most people handle this when they first start worrying about stability:

void apply_force(RigidBody *body, Vector2 force) {
    if (body == NULL) {
        fprintf(stderr, "Error: body is NULL in apply_force\n");
        return; 
    }
    body->velocity.x += force.x / body->mass;
    body->velocity.y += force.y / body->mass;
}

On the surface, this looks safe. The program doesn't crash. But here is the problem: if body is NULL, something is fundamentally broken in your game logic. Some other part of your code just tried to move a non-existent object. By printing a message and returning, you've allowed the program to keep running in an invalid state. You'll likely encounter a much weirder bug ten functions later, and by then, the original cause—the NULL pointer—is long gone from the call stack. I call this "silent failure," and it's a nightmare to debug.

Failing fast and loud

This is where assert.h comes in. An assertion isn't meant to handle "expected" errors (like a user typing a letter instead of a number); it's meant to catch "impossible" errors—things that should never happen if your logic is correct. I want the program to explode the second an invariant is violated.

If we rewrite that function using assert(), it looks like this:

#include <assert.h>

void apply_force(RigidBody *body, Vector2 force) {
    assert(body != NULL);
    
    body->velocity.x += force.x / body->mass;
    body->velocity.y += force.y / body->mass;
}

The difference is subtle but massive. When assert(body != NULL) fails, the program doesn't just print a message; it aborts immediately and tells you exactly which file and which line number caused the failure. It forces you to fix the bug in the calling code rather than putting a bandage on the receiving function. You stop the bleeding immediately.

The cost of visibility

Now, you might be thinking, "I can't ship my game to customers with code that crashes if a pointer is NULL." You're right. You shouldn't. But here's the magic of assert: it's designed to disappear.

Assertions are only active during development. If you define the macro NDEBUG (No Debug) before including assert.h, the compiler completely removes every assert() call from your binary. They cost you nothing in your production build. This creates a perfect trade-off: you get aggressive, loud, and helpful error reporting while you're coding, but your end-user gets a lean, high-performance binary without the overhead of constant internal sanity checks.

Just a word of caution: never put actual logic inside an assertion. Don't do something like assert(do_critical_cleanup() == 1);. Since assertions vanish in production, your cleanup code will vanish too, and you'll be scratching your head wondering why your memory is leaking in the release build. Keep assertions as pure observations of state.




📋 Practical Task

Hardening the Stack Pop Operation

You are maintaining a simple Stack library. The pop function is currently "too polite"—it returns NULL when the stack is empty, forcing the user to check for NULL every single time they pop an item. However, in this specific engine, popping from an empty stack is considered a critical programmer error (a logic bug) that should be caught during development.

Modify the following code to use assert.h. Remove the manual if check and replace it with an assertion that ensures the stack->top is not -1 before attempting to access the array.

#include <stdio.h>
#include <stdlib.h>
// TODO: Include the necessary header for assertions

typedef struct {
    int *items;
    int top;
    int capacity;
} Stack;

int pop(Stack *stack) {
    // Current naive implementation
    if (stack->top == -1) {
        fprintf(stderr, "Stack Underflow!\n");
        return -1; 
    }

    return stack->items[stack->top--];
}

int main() {
    Stack myStack = { .items = malloc(10 * sizeof(int)), .top = -1, .capacity = 10 };
    
    // This should trigger the assertion and crash the program
    printf("Popping: %d\n", pop(&myStack));
    
    free(myStack.items);
    return 0;
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.