C Assertions with assert.h: Detect Programming Errors Early

Assertions are a useful debugging technique in C. They allow a program to state assumptions that should always be true at a particular point in execution. If an assertion fails, the program reports the failure and normally terminates.

The standard assert macro is provided by the assert.h header.

What Is an Assertion?

An assertion checks whether a condition is true. If the condition is true, execution continues. If it is false, the assertion fails and the implementation reports diagnostic information before terminating the program.

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

int main(void)
{
    int value = 10;

    assert(value > 0);

    printf("Value is positive.\n");
    return 0;
}

How assert() Works

The assert macro evaluates a scalar expression. When assertions are enabled and the expression evaluates to zero, the assertion fails.

C
assert(expression);

If expression evaluates to a nonzero value, nothing happens and execution continues. If it evaluates to zero, the implementation writes diagnostic information and calls abort().

A Simple Failed Assertion

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

int main(void)
{
    int age = -5;

    assert(age >= 0);

    printf("Age: %d\n", age);
    return 0;
}

Because age >= 0 is false, the assertion fails. A typical implementation reports the failed expression, source file, line number, and function before terminating the program.

Why Use Assertions?

Assertions are primarily intended to detect programming errors and broken assumptions during development and testing.

  • Verify internal invariants
  • Detect impossible program states
  • Check assumptions made by a function
  • Catch bugs close to where they occur
  • Document conditions that should always be true

Assertions as Executable Documentation

An assertion can communicate an assumption directly in the source code.

C
void processIndex(int index, int length)
{
    assert(index >= 0);
    assert(index < length);

    /* Safe to use index under the asserted invariant. */
}

The assertions tell future readers that index is expected to be within the valid range at this point in the program.

Assertions for Function Invariants

Assertions are especially useful for checking internal invariants that should hold after a sequence of operations.

C
#include <assert.h>

struct Counter
{
    int value;
};

void increment(struct Counter *counter)
{
    assert(counter != NULL);

    counter->value++;

    assert(counter->value >= 0);
}

The second assertion expresses an expected invariant. Whether that invariant is actually valid depends on the wider design and possible integer overflow, so assertions should represent carefully considered assumptions rather than guesses.

assert() and NULL Pointers

An assertion can be useful for detecting an internal programming error involving an unexpected null pointer.

C
#include <assert.h>

void printName(const char *name)
{
    assert(name != NULL);

    /* Use name here. */
}

However, whether NULL should cause an assertion failure depends on the function's contract. If NULL is a legitimate input that should be rejected gracefully, normal error handling is more appropriate.

Assertions vs Error Handling

Assertions and error handling serve different purposes.

AssertionsError Handling
Detect programming errors and violated assumptionsHandle expected runtime failures
Usually terminate the program when enabledUsually lets the program recover or report failure
Useful for internal invariantsUseful for invalid user input, missing files, network failures, etc.
Can be disabled with NDEBUGShould remain part of normal program behavior

Do Not Use assert() for User Input Validation

User input can legitimately be invalid. It should normally be validated using ordinary control flow rather than an assertion.

C
#include <stdio.h>

int main(void)
{
    int age;

    printf("Enter age: ");
    scanf("%d", &age);

    if (age < 0)
    {
        fprintf(stderr, "Invalid age.\n");
        return 1;
    }

    printf("Age: %d\n", age);
    return 0;
}

This is preferable to assert(age >= 0) because invalid user input is an expected runtime condition, not necessarily a programming bug.

The NDEBUG Macro

Assertions can be disabled by defining NDEBUG before including assert.h.

C
#define NDEBUG
#include <assert.h>

assert(0);

With NDEBUG defined, the assert macro expands so that the expression is not evaluated.

Disabling Assertions for Release Builds

Many projects enable assertions during development and testing while defining NDEBUG for release builds. This can remove assertion overhead and prevent internal debugging checks from terminating a production application.

A Critical Rule: Never Put Required Side Effects in assert()

Because the expression may not be evaluated when NDEBUG is defined, an assertion must not contain an operation that the program relies on for its behavior.

C
int value = 0;

assert(value++ == 0);

With assertions enabled, value may be incremented. With NDEBUG enabled, the expression is not evaluated and value remains unchanged. This creates different program behavior between debug and release builds.

Bad Assertion Example

C
assert(fclose(file) == 0);

This is dangerous if closing the file is required for correct program behavior because fclose may not be called at all when assertions are disabled. Perform the operation separately and then check its result.

C
int result = fclose(file);
assert(result == 0);

Assertions and Expressions

The expression passed to assert should ideally be simple, deterministic, and free of side effects.

C
assert(index >= 0 && index < length);
assert(pointer != NULL);
assert(size <= MAX_SIZE);

Using Assertions in Algorithms

C
#include <assert.h>

int binarySearch(const int values[], int length, int target)
{
    assert(values != NULL || length == 0);
    assert(length >= 0);

    /* Search implementation. */
    (void)target;
    return -1;
}

Assertions can make assumptions about an algorithm explicit. The conditions should describe genuine invariants rather than conditions that are merely expected most of the time.

Assertions Do Not Make Code Safe by Themselves

An assertion is a diagnostic mechanism, not a memory-safety mechanism. If a program accesses invalid memory before reaching the assertion, the assertion cannot protect it.

Assertions and Defensive Programming

Assertions can be part of defensive programming, but not every defensive check should be an assertion. A useful distinction is whether the condition represents a programmer mistake or a normal condition that the program should handle.

Good Assertion Candidates

  • Internal state that should be impossible to reach
  • Preconditions guaranteed by an internal API contract
  • Postconditions that a function promises to establish
  • Data structure invariants
  • Assumptions made by an algorithm

Poor Assertion Candidates

  • User-provided input
  • Missing files that users are allowed to choose
  • Network failures
  • Memory allocation failures that the program intends to handle
  • Conditions that must still be checked in production
  • Operations with required side effects

Assertions and Return Values

If a function can fail as part of normal operation, return an error status and let the caller decide what to do.

C
#include <stddef.h>

int divide(int a, int b, int *result)
{
    if (b == 0 || result == NULL)
    {
        return 0;
    }

    *result = a / b;
    return 1;
}

If a NULL result pointer violates an internal contract rather than being a valid caller error, an assertion could instead be appropriate. The correct choice depends on the API contract.

Assertions and Release Builds

When NDEBUG is enabled, assertions are disabled. Therefore, code must remain correct even when every assert expression disappears.

C
int value = calculateValue();

assert(value >= 0);

useValue(value);

If value must be nonnegative for production correctness, the program should enforce that requirement through normal logic rather than relying solely on assert.

Assertion Messages

The standard assert macro does not provide a separate message parameter. A common idiom is to combine a condition with a string literal using the logical AND operator.

C
assert(pointer != NULL && "pointer must not be NULL");

Because a non-empty string literal evaluates to a nonzero pointer value, the message does not change the result when pointer is valid. If the condition fails, the assertion expression contains the string as part of the diagnostic expression. This is a common idiom, but the exact diagnostic presentation is implementation-dependent.

Static Assertions with _Static_assert

C11 introduced _Static_assert for compile-time assertions. Unlike assert(), it checks a constant expression during translation rather than during program execution.

C
#include <limits.h>

_Static_assert(CHAR_BIT >= 8, "A byte must contain at least 8 bits");

C23 also provides static_assert as a keyword, while _Static_assert remains available according to the C standard version and implementation.

Runtime vs Compile-Time Assertions

Featureassert()_Static_assert / static_assert
When checkedDuring program executionDuring translation
ExpressionRuntime scalar expressionConstant expression suitable for static assertion
Can inspect runtime values?YesNo
Can be disabled with NDEBUG?YesNo
Primary purposeRuntime debugging assumptionsCompile-time constraints

Assertions for Data Structure Invariants

C
#include <assert.h>

struct Buffer
{
    int size;
    int capacity;
};

static void checkBuffer(const struct Buffer *buffer)
{
    assert(buffer != NULL);
    assert(buffer->size >= 0);
    assert(buffer->capacity >= 0);
    assert(buffer->size <= buffer->capacity);
}

A helper such as checkBuffer can be called during development to detect corruption of a data structure close to where it occurs.

Assertions and Unit Testing

Assertions can help identify incorrect internal states during tests, but assert() itself is not a complete testing framework. Production-quality test suites normally use a testing library or explicit test-result checks that can report multiple failures without immediately terminating the test process.

Common Mistakes

  • Using assert() to validate ordinary user input
  • Putting required side effects inside an assertion
  • Assuming assertions remain enabled in every build
  • Using assertions as a replacement for error handling
  • Writing assertions for conditions that are expected to fail normally
  • Assuming an assertion prevents memory errors before it is reached
  • Forgetting that assert() normally terminates the program when it fails

Best Practices

  • Use assert() to document and check programmer assumptions
  • Keep assertion expressions free of side effects
  • Use normal control flow for expected runtime failures
  • Never rely on an assertion for essential production behavior
  • Use _Static_assert or static_assert for compile-time requirements
  • Check important data structure invariants during development and testing
  • Make assertions specific enough to identify the violated assumption
  • Ensure the program remains correct when NDEBUG disables assertions

Quick Reference

ToolPurpose
assert(expression)Check a runtime programming assumption
NDEBUGDisable standard runtime assertions
_Static_assertPerform a compile-time assertion in C11 and later
static_assertC23 keyword for compile-time assertions

Practice Exercises

  • Write a function that uses assert() to verify an array length is nonnegative
  • Create a data structure invariant checker using multiple assertions
  • Find an example where putting a function call inside assert() causes different debug and release behavior
  • Rewrite an assertion-based input validation example using normal error handling
  • Use _Static_assert to verify assumptions about integer sizes
  • Create a function with documented preconditions and add suitable assertions
  • Build a small program with NDEBUG enabled and verify that assertions are not evaluated

Conclusion

Assertions are one of the simplest ways to expose incorrect assumptions in C programs. The assert() macro is particularly useful for internal invariants, algorithm assumptions, and programmer errors during development. However, assertions can be disabled, so they should never replace required validation or error handling. For compile-time requirements, use static assertions, while ordinary runtime failures should be handled explicitly.

Note: Note: The exact diagnostic text produced by a failed assert() is implementation-dependent. Assertions are intended primarily for detecting programming errors and assumptions, not for handling expected runtime failures.