Debugging fundamentals10 min read

Assert Statements: Where They Help and Where They Hurt

Assert statements are a double-edged sword. Used correctly, they catch bugs early and document invariants. Used carelessly, they can mask failures and create security holes. Here's how to wield them effectively.

assertdebuggingdefensive programmingtestingerror handling

I've seen assert statements cause more debugging pain than they prevent. Not because asserts are bad — they're not — but because developers misunderstand when to use them. The result is code that fails silently in production or, worse, skips critical checks entirely.

The core problem is simple: assert statements are not a substitute for error handling. In many languages, asserts are compiled away or disabled in production. That means the crash you expected during development becomes a silent corruption in the field.

The One Rule of Asserts

Here's the rule I follow: Asserts document invariants — things that must be true if the code is correct. They are not for validating external input, handling user errors, or checking things that can fail due to runtime conditions.

If a condition can be false because of bad data from a file, network, or user — don't assert it. Use an if statement and throw a proper exception or return an error. If a condition can only be false because of a bug in your own code, then assert it.

warning

Never validate user input with assert. In Python, running with python -O removes all asserts. In C, defining NDEBUG disables assert(). Your security boundary just vanished.

A Real-World Incident: The Silent Data Loss

The Silent Data Loss

  1. 09:00Deploy new version of payment processing service.
  2. 12:30Support receives report of missing transactions for some users.
  3. 14:00Engineers discover assert statement in critical path that was meant to check record count.
  4. 14:15Realize assert had side effect: it incremented a counter. With asserts disabled, counter never incremented, causing subsequent logic to skip processing.

Lesson

Never put side effects inside assert expressions. Use a separate variable and assert on that. Even better, don't rely on asserts for business logic at all.

This incident happened at a former company. The assert was in a loop processing transactions. The developer wrote assert(counter++ < MAX_TRANSACTIONS). In development, it worked fine. In production, asserts were disabled, so counter never incremented, and the loop processed unlimited transactions, corrupting the database. It took two days to recover the data.

When to Use Asserts (with Examples)

Asserts shine in three specific scenarios: enforcing preconditions/postconditions, checking impossible states, and documenting assumptions in complex algorithms. Let me show each with concrete code.

1. Preconditions and Postconditions

Using asserts to enforce preconditions and postconditions on a function. These catches programming errors during development.
def calculate_discount(price: float, rate: float) -> float:
    assert price > 0, f"Price must be positive, got {price}"
    assert 0 < rate <= 1, f"Rate must be in (0,1], got {rate}"
    result = price * rate
    assert 0 < result < price, f"Discount must be between 0 and price, got {result}"
    return result

Notice the asserts are not for user input. If price came from user input, you'd validate it with an if statement. But if price is computed internally and should always be positive, an assert documents that invariant and catches bugs quickly.

2. Impossible States

Using assert to catch an impossible state. If you reach the else, something went wrong upstream.
def handle_status(status: str) -> None:
    if status == "active":
        # ...
    elif status == "inactive":
        # ...
    else:
        # This should never happen
        raise AssertionError(f"Unexpected status: {status}")

I use assert False or raise AssertionError for this pattern. It's more explicit than a bare assert and works even if asserts are disabled (if you raise explicitly). Some teams prefer to raise a custom exception. That's fine — the point is to crash hard on bugs, not silently continue.

3. Complex Algorithms

An assert documenting that the loop must return if the target exists, and the final return is only for invalid inputs.
def binary_search(arr, target):
    left, right = 0, len(arr) - 1
    while left <= right:
        mid = (left + right) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    assert False, "Unreachable if input is valid"
    return -1

This assert tells the reader: 'If you reach here, either the input was invalid or there's a bug.' It's a safety net. But note: if you expect invalid inputs, you should handle them explicitly, not with an assert.

When to Avoid Asserts

  • arrow_rightValidating user input, API parameters, or file data.
  • arrow_rightChecking conditions that could fail due to resource constraints (e.g., memory allocation, network timeouts).
  • arrow_rightImplementing security checks (authentication, authorization, input sanitization).
  • arrow_rightAny condition that must be enforced in production — use proper exceptions instead.
  • arrow_rightExpressions with side effects (function calls, mutations).

The rule of thumb: if a failure would be a bug in your code, use assert. If a failure could be caused by anything else (user, environment, system), use proper error handling.

lightbulb

In Python, you can simulate production behavior by running with -O: python -O your_script.py. Test that asserts don't break your logic when disabled.

Language Differences Matter

Not all languages treat asserts the same way. In C, assert() is a macro that expands to nothing when NDEBUG is defined. In Java, assertions are disabled at runtime by default and must be enabled with -ea. In Python, assert is a statement that is removed with -O. In Go, there is no built-in assert — you write your own or use a testing package.

Know your language's behavior. If asserts are disabled by default in production, never use them for anything that must run. If they always run (like in JavaScript with console.assert), they still shouldn't be used for validation because they don't throw exceptions — they just log a warning.

A Better Pattern: Custom Assert Functions

A custom invariant check that logs the error before crashing. You can tailor this to your environment (e.g., send to monitoring).
import logging

def invariant(condition: bool, message: str = ""):
    """Custom assert that always runs in debug mode."""
    if __debug__ and not condition:
        logging.error(f"Invariant failed: {message}")
        raise AssertionError(message)

This gives you control: you can decide whether to log, crash, or both. Some teams prefer to always run invariant checks in production but never crash the process — they log the error and continue. That's a valid choice, but it's a different pattern from language-level asserts.

Asserts in Unit Tests vs. Production Code

Unit test frameworks have their own assert functions (e.g., assertEqual, assertTrue). These are separate from language-level asserts. They always run and are designed to fail tests. That's fine — use them liberally.

The confusion happens when developers use language-level asserts inside production code and think they're writing tests. They're not. Production asserts are documentation and debugging aids, not test cases. If you want to verify behavior, write a unit test.

Asserts are for catching bugs, not for handling failures. If you can't crash the program on an invariant violation, you shouldn't be using an assert.

Final Thoughts

I've seen codebases with hundreds of asserts and no error handling — and codebases with zero asserts and perfect error handling. The first is fragile, the second is hard to debug. The sweet spot is using asserts sparingly to document the non-negotiable truths of your code, and using proper error handling for everything else.

When you write an assert, ask: 'If this fails, is the only correct response to crash immediately?' If yes, use an assert. If there's any recovery possible — even logging and continuing — then an assert is the wrong tool.

Frequently asked questions

Should I use assert or if/throw for input validation?

Always use if/throw for input validation. Asserts are typically disabled in production, so they won't catch invalid input at runtime. Use asserts only for internal invariants that should never be false if the code is correct.

Why do asserts disappear in production Python?

Python runs with -O (optimize) flag, which removes assert statements. Also, the __debug__ global is False. So assert False becomes a no-op. This is why relying on asserts for validation is dangerous.

Can I put side effects like function calls inside an assert?

No. If asserts are disabled, the side effect never happens, leading to silent data corruption. Always compute the expression before the assert and store it in a variable if needed.

How do asserts differ from unit test assertions?

Unit test assertions (e.g., assertEqual) are part of the test framework and always run during tests. Code-level asserts (e.g., Python's assert) are built into the language and may be disabled in production. They serve different purposes.