A common mistake when first encountering assert() is to treat it as a general-purpose runtime check — a way to validate user input or to guard against missing files.
It is none of those things.
Assertions and error handling solve different problems, and using one where the other belongs leads to programs that either crash on the user or silently mask real bugs.
This Nibble covers differences between assert() and static_assert and how to decide between assertions and error handling in your own code.
assert() checks runtime invariants
Assertions allow us to detect an invalid parameter to a function via a conditional statement.
It will return true unless there is a bug in the program. If it evaluates to false, the program is terminated via std::abort.
The error message identifies which line the program failed, making it extremely useful for debugging.
Consider the following.
#include <cassert>
#include <iostream>
double applyDiscount(double originalPrice, double discountPercent)
{
assert(discountPercent >= 0.0 && discountPercent <= 100.0);
double savings = originalPrice * (discountPercent / 100.0);
return originalPrice - savings;
}
int main()
{
double price{ 50.0 };
double badDiscount{ 125.0 };
std::cout << "Final: $" << applyDiscount(price, badDiscount) << '\n';
}
// Output
main.cpp:7: Assertion `discountPercent >= 0.0 && discountPercent <= 100.0' failed.
Aborted
The program aborted.
In order for applyDiscount() to operate, discountPercent must be ≥ 0 and ≤ 100. Rather than let the function assume this, we can enter an assert(). If the condition isn’t fulfilled, the function doesn’t proceed. This is excellent for enforcing preconditions. In this example, the program aborted.
Making your assert statements more descriptive
You can make your assert’s more descriptive by appending a string
assert(discountPercent >= 0.0 && discountPercent <= 100.0 &&
"Please enter a valid discount");
// Output
main.o: /tmp/MHoPs04hi7/main.cpp:11: double applyDiscount(double, double):
Assertion `discountPercent >= 0.0 && discountPercent <= 100.0 &&
"Please enter a valid discount"' failed.
Aborted
Assertions disappear in release builds
The most important practical fact about assert(): when the macro NDEBUG is defined, assert(expr) becomes a no-op. The expression is not evaluated at all. Most build systems define NDEBUG automatically in release configurations, so:
- Debug builds: assertions run, catch bugs.
- Release builds: assertions vanish, costing zero runtime.
This is what makes assertions cheap enough to scatter liberally through development code. It also creates one trap worth knowing: never put an expression with a side effect inside assert().
assert(removeItem(itemId)); // BUG: removeItem doesn't run in release
The function is silently dropped in release builds and the program behaves differently from how the developer tested it. The defensive idiom is to evaluate first, assert second:
bool removed = removeItem(itemId);
assert(removed);
static_assert checks compile-time invariants
C++ offers another type of assertion.
A static assert is evaluated at compile-time rather than run-time. This means the condition must be a constant expression. If the condition is not true, the diagnostic message is printed.
This means you can’t use static_assert to enforce user input.
// Syntax
static_assert(condition, diagnostic_message)
static_assert(sizeof(long) == 8, "long must be 8 bytes")
Favor
static_assertoverassert()whenever possible.
Assertions are different from error handling
Assertions are used to detect programming errors during development by documenting assumptions about things that should never happen. And if they do happen, it’s the fault of the programmer. Assertions do not allow recovery from errors (after all, if something should never happen, there’s no need to recover from it).
Error handing is when we need to handle errors. The world is unpredictable and complex. Our program must anticipate maybe recoverable or unrecoverable issues (the program shuts down).
Use assertions to detect programming errors, incorrect assumptions, or conditions that should never occur. There is no recovery from the error. The program is terminated via std::abort. E.g. A private helper function is called with an out-of-range index.
Use error handling for issues that we expect will occur during the normal operation of your program. This expands beyond programming errors. The real world is unpredictable.
Packets get dropped during transmission, a user doesn’t exist, a session timed out, a user typing a negative number when prompted for an array length. These are all errors that must be treated.
Use both in cases where something isn’t supposed to occur, but we want to fail gracefully if it does. Error handling allows the program to recover from errors, assertions do not as std::abort() is called.
Takeaway
Reach for assert() when a condition would only fail because of a bug, and reach for static_assert when the same kind of condition can be checked at compile time. Reach for error handling when the condition can occur even with correct code.