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.”
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:
maincreates an array containing42and99.maincallsget_first(numbers, 2).values != NULLis true becausenumberspoints to the array.length > 0is true because the length is2.- The function returns
values[0], which is42. printfprints42.
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.
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 approach | Best for | What happens on failure | Suitable for recoverable failures? |
|---|---|---|---|
assert(condition) | Programmer assumptions and internal invariants | Diagnostic output, then abort() when enabled | Usually no |
if with an error return | Invalid input and expected runtime problems | Caller receives an error result | Yes |
| C++ exception | Operations that cannot complete and need error propagation | Control transfers to a matching catch if one exists | Yes, when the project uses exceptions |
| Logging | Recording context for later diagnosis | Program may continue or return an error | Yes |
| Unit test assertion | Verifying expected behavior during tests | The test fails | Not 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.”
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
NDEBUGbefore 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.
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.
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.