Question

Using assert() in C and C++: Benefits, Risks, and Best Practices

gocppcerror-handlingassert

Question

The Go language designers argue that assertions can encourage programmers to avoid proper error handling and reporting. They emphasize that servers should recover from non-fatal errors rather than crash, and that errors should be clear for developers unfamiliar with the code.

How do these arguments apply to C and C++? What are the advantages and disadvantages of using assert()? When should assertions be used instead of normal error handling?

Short Answer

By the end of this page, you will know that assert() is a debugging tool for checking programmer assumptions, not a replacement for handling expected runtime failures. You will learn how it behaves in C and C++, when it is useful, and how to write assertions safely.

Concept

assert() checks whether a condition that must be true at a particular point in the program is actually true.

In C, include <assert.h>. In C++, include <cassert>:

assert(pointer != NULL);

If the condition is true, execution continues normally. If it is false, the assertion fails: the implementation prints diagnostic information and terminates the program by calling abort().

Assertions are useful for detecting bugs in the program's logic, such as:

  • A function receiving an internal value that violates its documented contract.
  • An array index that should already have been validated.
  • A data structure whose internal state is inconsistent.

They are not appropriate for failures that can occur during normal operation, including invalid user input, missing files, network timeouts, or allocation failures. Those conditions need explicit handling because a robust program may need to report the problem, retry, reject input, return an error, or continue serving other requests.

A key property of the standard C and C++ assert macro is that defining NDEBUG before including the assertion header disables assertions. In that configuration, the asserted expression is not evaluated at all. This makes assertions useful for debug checks, but it also means they must not perform essential work.

Mental Model

Think of an assertion as a smoke alarm inside a workshop.

  • It is installed to reveal a condition that should never happen if the workshop is being run correctly.
  • When it sounds, everyone stops and investigates the underlying fault.
  • It is not the normal procedure for handling a customer arriving with an invalid order. That case should be handled politely and predictably.

Similarly, use an assertion for “this internal invariant cannot be false.” Use normal error handling for “this can happen in the outside world, so the program needs a response.”

Take Quiz

Syntax and Examples

In C:

#include <assert.h>

int square(int value) {
    assert(value >= 0);
    return value * value;
}

In C++:

#include <cassert>

int square(int value) {
    assert(value >= 0);
    return value * value;
}

The assertion documents an assumption: this version of square accepts only non-negative values. During a build where assertions are enabled, square(-2) stops the program and identifies the failed condition.

However, do not use an assertion to validate external input:

int read_age(int age) {
    if (age < 0 || age > 130) {
        return -1;  // Report invalid input through the function's contract.
    }

    return age;
}

Invalid input is expected to be possible. The caller needs a usable error result rather than an unexpected process termination.

Step by Step Execution

Consider this C program:

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

int get_first(const int values[], int length) {
    assert(values != NULL);
    assert(length > 0);
    return values[0];
}

int main(void) {
    int numbers[] = {42, 99};
    printf("%d\n", get_first(numbers, 2));
    return 0;
}

Execution proceeds as follows:

  1. main creates an array containing 42 and 99.
  2. main calls get_first(numbers, 2).
  3. values != NULL is true because numbers points to the array.
  4. length > 0 is true because the length is 2.
  5. The function returns values[0], which is 42.
  6. printf prints 42.

Now consider get_first(NULL, 0) with assertions enabled. The first assertion fails, a diagnostic is produced, and the process is aborted before it dereferences the null pointer.

If NDEBUG is defined, both checks disappear. Calling get_first(NULL, 0) would then attempt values[0], causing undefined behavior. This is why assertions cannot be your only protection against invalid data from outside the program.

Real World Use Cases

Assertions are commonly useful in these situations:

  • Data structures: Verify that a linked-list node is non-null before an internal operation that requires it.
  • Parser internals: Check that a token type is one the parser has already guaranteed.
  • State machines: Verify that a transition is allowed from the current state.
  • Algorithm invariants: Check that a loop index remains within a known valid range.
  • Debug builds and tests: Catch invalid internal states close to the code that created them.

Normal error handling is better for these situations:

  • A user uploads an unsupported file.
  • A configuration value is missing or malformed.
  • A file cannot be opened.
  • A network request times out.
  • A server receives malformed API data.

In each of these cases, the program should generally return a clear error, log relevant context, or continue handling other work.

Take Quiz

Real Codebase Usage

In production code, developers often combine explicit validation with assertions for internal guarantees.

#include <assert.h>
#include <stddef.h>

typedef enum {
    LOOKUP_OK,
    LOOKUP_NOT_FOUND,
    LOOKUP_INVALID_ARGUMENT
} LookupResult;

LookupResult find_user(const char *name, int *user_id) {
    if (name == NULL || user_id == NULL) {
        return LOOKUP_INVALID_ARGUMENT;
    }

    /* Imagine a real lookup here. */
    int found_id = 17;

    assert(found_id > 0);  // Internal invariant: stored IDs are positive.
    *user_id = found_id;
    return LOOKUP_OK;
}

This pattern separates responsibilities:

  • A guard clause handles invalid arguments that a caller might provide.
  • An error code lets the caller decide how to recover or report the problem.
  • An assertion checks an internal assumption that indicates a programming or data-integrity bug if violated.

For libraries, daemons, services, and embedded systems, consider the consequences of aborting. A failed assertion can be appropriate in a development build or for an unrecoverable corrupted state, but it may be unacceptable if one bad request can stop a long-running process.

Common Mistakes

Using assert() for required validation

Broken approach:

assert(filename != NULL);
FILE *file = fopen(filename, "r");

If assertions are disabled, filename may be null. Also, fopen can fail even for a valid filename.

Better:

if (filename == NULL) {
    return NULL;
}

FILE *file = fopen(filename, "r");
if (file == NULL) {
    return NULL;
}

Putting side effects inside an assertion

Broken approach:

assert(index++ < length);

When NDEBUG is defined, index++ is not evaluated. The program behaves differently between builds.

Better:

index++;
assert(index <= length);

Treating an assertion as user-facing reporting

An assertion message and crash trace may help a developer, but they are usually not a useful response for an end user or an API client. Return a structured error or show a clear message for expected failures.

Assuming assert() always runs

The standard macro is normally disabled when NDEBUG is defined before <assert.h> or <cassert> is included. Build configuration matters.

Asserting conditions that can legitimately fail

Do not assert that a file exists, a network connection succeeds, or memory allocation always succeeds. Those are environmental conditions, not necessarily programming bugs.

Comparisons

Tool or approachBest forWhat happens on failureSuitable for recoverable failures?
assert(condition)Programmer assumptions and internal invariantsDiagnostic output, then abort() when enabledUsually no
if with an error returnInvalid input and expected runtime problemsCaller receives an error resultYes
C++ exceptionOperations that cannot complete and need error propagationControl transfers to a matching catch if one existsYes, when the project uses exceptions
LoggingRecording context for later diagnosisProgram may continue or return an errorYes
Unit test assertionVerifying expected behavior during testsThe test failsNot runtime error handling

assert() and an if statement can both check a Boolean condition, but they communicate different intent:

  • assert(condition) means: “If this is false, there is a bug in the program or a violated internal contract.”
  • if (!condition) means: “This can happen, and this code defines what to do next.”
Take Quiz

Cheat Sheet

#include <assert.h>   // C
#include <cassert>    // C++

assert(expression);
  • Use assertions for internal invariants and programmer errors.
  • Use explicit error handling for invalid input and environmental failures.
  • A failed enabled assertion prints diagnostic information and calls abort().
  • Define NDEBUG before including the header to disable standard assertions.
  • When disabled, the assertion expression is not evaluated.
  • Never put required side effects in assert(...).
  • Do not rely on assertions to prevent undefined behavior in release builds.
  • Prefer a clear error return, exception policy, or error object when callers must recover.

FAQ

Is assert() evil in C or C++?

No. It is valuable when used as a debugging and invariant-checking tool. It becomes harmful when used instead of required runtime error handling.

Should I use assert() for user input?

Usually no. Users, files, networks, and APIs can provide invalid data during normal operation. Validate it and return or report an error.

Does assert() run in release builds?

Not necessarily. Standard assertions are disabled when NDEBUG is defined before the assertion header is included. Check your build configuration.

What happens when a C or C++ assertion fails?

With assertions enabled, the implementation writes diagnostic information and calls abort(), terminating the process abnormally.

Why are side effects inside assert() dangerous?

The whole expression disappears when assertions are disabled. An increment, function call, assignment, or resource operation inside the assertion may never occur in another build.

Should a server use assertions?

It can use them for internal bugs during development and for genuinely unrecoverable invariants, but it should not crash because a client sent bad input or because a recoverable operation failed.

Can assertions replace tests?

No. Assertions check conditions during execution. Tests deliberately exercise many inputs and scenarios to verify behavior.

Take Quiz

Mini Project

Description

Build a small C function that safely returns the first item in an integer array. The project demonstrates the difference between handling data a caller may provide incorrectly and asserting an internal condition after validation.

Goal

Return the first array value on success and return an error code for invalid caller input without relying on assert() for safety.

Requirements

  • Create a function that receives an integer array, its length, and an output pointer.
  • Return an error code when the array pointer, output pointer, or length is invalid.
  • Store the first array value in the output pointer on success.
  • Use an assertion only for an internal condition that should be guaranteed after validation.
  • Demonstrate one successful call and one invalid call in main.
Take Quiz

Keep learning

Related questions

View more