Skip to content

C Assertions — Runtime Debugging with assert.h and Static Assertions

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about C Assertions. We cover key concepts, practical examples, and best practices to help you master this topic.

C assertions using assert.h provide a portable mechanism for runtime invariant checking, where the assert macro evaluates a condition and aborts the program with a diagnostic message if it is false.

What You Will Learn

  • Using assert() for runtime precondition and postcondition checks
  • Static assertions with _Static_assert (C11) for compile-time validation
  • Enabling assertions in debug builds and disabling them in release
  • Writing custom assertion macros with additional diagnostics
  • Assertion best practices for Defensive Programming

Why It Matters

Bugs that manifest far from their root cause are the hardest to debug. Assertions catch bugs at the point of failure with a clear message: "assertion failed: file.c:42: ptr != NULL." This is vastly more useful than a segmentation fault three functions later. In production code like Durga Antivirus Pro, static assertions validate critical assumptions at compile time: _Static_assert(sizeof(FileHeader) == 64, "FileHeader must be exactly 64 bytes") prevents subtle bugs when the struct layout changes.

Real-World Use

A network protocol handler assumes a packet header is exactly 24 bytes. An engineer adds a field without updating the alignment, making the struct 28 bytes. Without a static assertion, this goes unnoticed until packets fail to parse at runtime. With _Static_assert(sizeof(PacketHeader) == 24, "Protocol requires 24-byte header"), the build fails immediately with a clear message.

Learning Path

flowchart LR
  A[setjmp & longjmp] --> B[Assertions\nYou are here]
  B --> C[The Preprocessor]
  style B fill:#f90,color:#fff

Runtime Assertions

The assert macro from assert.h checks a condition at runtime. If the condition is false (zero), it prints the file, line, and expression, then calls abort().

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

int divide(int a, int b) {
    // Precondition: divisor must not be zero
    assert(b != 0 && "Division by zero!");
    return a / b;
}

int main() {
    printf("100 / 5 = %d\n", divide(100, 5));

    // This will trigger the assertion
    printf("100 / 0 = %d\n", divide(100, 0));

    return 0;
}

Output:

100 / 5 = 20
assert: assert.c:8: divide: Assertion 'b != 0 && "Division by zero!"' failed.
Aborted (core dumped)

Enabling and Disabling Assertions

Assertions are controlled by the NDEBUG macro. If NDEBUG is defined before including assert.h, all assert() calls expand to nothing:

// Uncomment this line to disable assertions in release builds
// #define NDEBUG

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

int main() {
    int x = 42;

    // This assertion is active only when NDEBUG is NOT defined
    assert(x == 42);
    printf("x is 42, assertion passed\n");

    // Without NDEBUG: assertion fails, program aborts
    // With NDEBUG: assertion ignored, program continues
    assert(x == 0 && "This should never happen");
    printf("This line only reached if NDEBUG is defined\n");

    return 0;
}

Compile with assertions for debug: gcc -g -O0 program.c -o program. Compile without assertions for release: gcc -DNDEBUG -O2 program.c -o program.

Static Assertions (C11)

Static assertions check conditions at compile time. They do not generate any runtime code:

#include <stdio.h>
#include <stdint.h>
#include <assert.h>  // For static_assert macro (C11)

// Compile-time size checks
_Static_assert(sizeof(int) >= 4, "int must be at least 4 bytes");
_Static_assert(sizeof(uint64_t) == 8, "uint64_t must be exactly 8 bytes");

// Platform assumptions
_Static_assert(CHAR_BIT == 8, "Only 8-bit bytes are supported");

struct Packet {
    uint16_t length;
    uint32_t sequence;
    uint8_t data[16];
};

_Static_assert(sizeof(struct Packet) == 22, "Packet struct must be 22 bytes (no padding)");

int main() {
    printf("All static assertions passed. struct Packet = %zu bytes\n",
           sizeof(struct Packet));
    return 0;
}

Output (if all assertions pass):

All static assertions passed. struct Packet = 22 bytes

If a static assertion fails, the compiler gives an error:

error: static assertion failed: "Packet struct must be 22 bytes (no padding)"

Custom Assertion Macro

Build a more informative assertion macro:

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

// Custom assert with formatted message
#define ASSERT(cond, fmt, ...) \
    do { \
        if (!(cond)) { \
            fprintf(stderr, "ASSERT FAILED: %s:%d: " fmt "\n", \
                    __FILE__, __LINE__, ##__VA_ARGS__); \
            abort(); \
        } \
    } while (0)

// Assert that also logs to a file
#define ASSERT_LOG(cond, msg) \
    do { \
        if (!(cond)) { \
            FILE *log = fopen("error.log", "a"); \
            if (log) { \
                fprintf(log, "ASSERT: %s:%d: %s\n", __FILE__, __LINE__, msg); \
                fclose(log); \
            } \
            fprintf(stderr, "ASSERT: %s:%d: %s\n", __FILE__, __LINE__, msg); \
            abort(); \
        } \
    } while (0)

typedef struct {
    int *data;
    int size;
} Vector;

int vector_get(Vector *v, int index) {
    ASSERT(v != NULL, "Vector pointer is NULL");
    ASSERT(index >= 0, "Index %d is negative", index);
    ASSERT(index < v->size, "Index %d out of bounds (size=%d)", index, v->size);
    return v->data[index];
}

int main() {
    Vector v = {NULL, 0};

    // This will trigger the assertion
    vector_get(&v, 0);

    return 0;
}

Output:

ASSERT FAILED: assert_custom.c:39: Index 0 out of bounds (size=0)
Aborted (core dumped)

Assertions for Invariants

Use assertions to check function postconditions and data structure invariants:

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

// Binary search with invariant checking
int binary_search(int arr[], int size, int target) {
    int left = 0, right = size - 1;

    while (left <= right) {
        // Invariant: target is in [left, right] if it exists
        assert(left >= 0 && left < size);
        assert(right >= 0 && right < size);
        assert(left <= right);

        int mid = left + (right - left) / 2;
        assert(mid >= left && mid <= right);

        if (arr[mid] == target) return mid;
        if (arr[mid] < target) left = mid + 1;
        else right = mid - 1;
    }

    // Postcondition: if we reach here, target is not in the array
    // Assert that the value is not present at any index
    // (This is expensive -- only for debug)
    for (int i = 0; i < size; i++) {
        assert(arr[i] != target);
    }

    return -1;
}

int main() {
    int arr[] = {1, 3, 5, 7, 9, 11, 13};
    int size = sizeof(arr) / sizeof(arr[0]);

    int found = binary_search(arr, size, 7);
    printf("Found 7 at index %d\n", found);

    int not_found = binary_search(arr, size, 8);
    printf("Search for 8 returned %d\n", not_found);

    return 0;
}

Output:

Found 7 at index 3
Search for 8 returned -1

Static Assertion in Struct Definitions

Prevent struct layout bugs with embedded static assertions:

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

// Network protocol header -- must be exactly 24 bytes
typedef struct {
    uint8_t  version;     // 1 byte
    uint8_t  flags;       // 1 byte
    uint16_t length;      // 2 bytes
    uint32_t source_ip;   // 4 bytes
    uint32_t dest_ip;     // 4 bytes
    uint16_t source_port; // 2 bytes
    uint16_t dest_port;   // 2 bytes
    uint32_t checksum;    // 4 bytes
    uint32_t sequence;    // 4 bytes
} __attribute__((packed)) ProtocolHeader;

// Verify size at compile time
_Static_assert(sizeof(ProtocolHeader) == 24,
    "ProtocolHeader must be exactly 24 bytes for wire format");

// ABI compatibility for serialization
_Static_assert(sizeof(uint8_t) == 1, "uint8_t size mismatch");
_Static_assert(sizeof(uint16_t) == 2, "uint16_t size mismatch");
_Static_assert(sizeof(uint32_t) == 4, "uint32_t size mismatch");

int main() {
    printf("ProtocolHeader size: %zu bytes (expected 24)\n",
           sizeof(ProtocolHeader));
    printf("All ABI compatibility checks passed.\n");
    return 0;
}

Common Mistakes

  1. Using assert for error handling: Assertions abort the program. They are for detecting programming bugs, not for handling runtime errors (file not found, network timeout). Use error codes or return values for expected errors.

  2. Assertions with side effects: assert(free(ptr), "freed") executes free in debug builds but not in release builds (when NDEBUG is defined). Never put function calls with side effects inside assert.

  3. Forgetting to include assert.h: Using assert without including the header produces a warning and the assertion is ignored. Always include assert.h.

  4. Disabled assertions in production: Release builds typically define NDEBUG, which removes all assertions. If you need runtime checks in production, write explicit if-statements that log and return errors.

  5. Static assertion on non-constant expressions: _Static_assert requires a compile-time constant expression. You cannot check runtime values. _Static_assert(x > 0, "x must be positive") fails if x is not a constant.

  6. Overusing expensive assertions: Assertions that traverse an entire data structure (like the postcondition in binary_search above) may slow debug builds significantly. Use them judiciously.

  7. Ignoring assertion failures in CI: Assertion failures in test runs indicate bugs. Treat them as test failures. Configure your CI to capture and report assertion messages.

Practice Questions

  1. How do you disable all assertions in a release build?
  2. What is the difference between assert() and _Static_assert()?
  3. Why should you never put function calls with side effects inside assert?
  4. How would you write a custom assertion that logs to syslog instead of stderr?
  5. Challenge: Write a macro ENSURE(cond, cleanup) that runs cleanup code if the assertion fails, then aborts. For example: ENSURE(ptr != NULL, free(buf)) should free the buffer before aborting.

Mini Project

Build a safe memory allocator with assertions:

  • Implement safe_malloc(size_t size) that checks for overflow: assert(size > 0 && size < SIZE_MAX / 2)
  • Implement safe_realloc(void *ptr, size_t new_size) with the same checks
  • Implement safe_free(void **ptr) that sets the pointer to NULL after freeing (double-free prevention)
  • Use static assertions to verify that sizeof(size_t) >= 4
  • Write tests that deliberately trigger assertion failures (comment out the assertions temporarily to run the tests)
  • Use the allocator in a Linked List implementation and run with AddressSanitizer to catch any remaining bugs

FAQ

Should I use assert or if-return for parameter validation?

Use assert for programming errors (internal invariants, preconditions) that should never happen. Use if-return for user-facing errors (invalid input, file not found) that can occur in normal operation.

Do assertions affect performance?

Only in debug builds. When NDEBUG is defined, assert() expands to nothing with zero runtime cost. Static assertions have zero runtime cost always.

Can I catch assertion failures?

Assertions call abort(), which terminates the program. You can install a signal handler for SIGABRT to log additional information before termination.

What is the difference between assert and static_assert?

assert checks conditions at runtime and is disabled in release builds. static_assert (_Static_assert) checks conditions at compile time and is always active.

How do I get a stack trace when an assertion fails?

Use a signal handler for SIGABRT that calls backtrace() from execinfo.h. On glibc systems, include <execinfo.h> and link with -rdynamic.

What is Next

Proceed to The Preprocessor to learn about macros, conditional compilation, and compile-time Code Generation. Then explore Header Files for modular program organization.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro

Home Browse C