← back to writing

Feb 2026

Asserts are more than enough

~8 min read

There's a pattern I've noticed in codebases that have grown beyond one person's head: error handling starts reasonable, then balloons into a second shadow program — layers of Result<T, E>, custom error enums, propagation macros, error contexts, error contexts for the error contexts. The actual logic drowns in ceremony. Meanwhile the bugs that kill products in production aren't subtle network timeouts — they're invariant violations that should never have been possible at all.

My take, after shipping a few real systems: most of what you want is just a well-placed assert.

What asserts actually do

An assert is a contract. It says: "If this is false, my program's logic is broken. Don't try to recover. Crash immediately and loudly." That's it. No allocation, no vtable dispatch, no log pipeline. Just a conditional and an abort.

#include <assert.h>

void push(Stack *s, int val) {
    assert(s != NULL);
    assert(s->len < s->cap);
    s->data[s->len++] = val;
}

Read this and you immediately know: the caller is responsible for not passing null and for not overfilling the stack. No return value to check. No error propagation. If either condition is violated, you get a crash with a file and line number pointing exactly at the bug. That's more useful than a silent NULL return that manifests as a crash three call frames later.

The two categories of failure

Before anything else, you need to internalize a distinction that most codebases blur:

The mistake most people make is treating programming errors as recoverable. They return false from a function when an index is out of bounds, and then some caller ignores it, and then you spend three hours in a debugger. An out-of-bounds index is a bug. Crash. Fix it. Move on.

Asserts in release builds

The common objection: "But asserts get compiled out with NDEBUG!" True for the standard assert(). The solution is trivial — write your own:

#define ASSERT(cond) \
    do { \
        if (!(cond)) { \
            fprintf(stderr, "ASSERT failed: %s\n  at %s:%d\n", \
                    #cond, __FILE__, __LINE__); \
            abort(); \
        } \
    } while(0)

Now it stays in release. "But what about performance?" If your hot path is assert-heavy enough that this matters, you have bigger problems. Most asserts are on function entry — they run once per call and the branch predictor will never miss on them. The overhead is immeasurable.

For the genuinely hot paths, you can add a DEBUG_ASSERT variant that strips in release. But be honest about when you actually need that. Usually you don't.

Asserts as documentation

This is the underrated benefit. When I see:

Matrix4 invert(Matrix4 m) {
    float det = determinant(m);
    assert(fabsf(det) > 1e-6f && "Matrix must be invertible");
    // ...
}

I know exactly what this function requires without reading a docstring (that may be stale). The assert is executable documentation. It runs on every call in debug. It cannot go out of sync with the code the way comments do. It is, in a very real sense, the best kind of documentation: verifiable.

Where asserts fall short

I'm not saying throw away error handling. File I/O fails. Memory allocation fails (sometimes). User input is always garbage. For these, you do need real error paths — and they should be explicit and visible at the call site.

The point isn't to assert everything. The point is to stop reaching for complex error machinery when you actually mean "this is a bug, catch it hard." Save the heavy lifting for failures that are genuinely expected and recoverable.

A practical heuristic

Before adding an error return to a function, ask: can this condition ever be true in correct code? If the answer is no — if it would only happen because of a bug in the caller — make it an assert. Reserve bool/Result/error codes for things that can fail in a fully correct program due to factors outside your control.

You'll find your codebases get smaller, faster to read, and easier to debug. The asserts will catch bugs at the exact moment they're introduced. And you'll stop wading through error propagation chains to diagnose what is, at the end of the day, a typo in an index calculation.

Most of the time, assert is more than enough. Trust it.