The C Path — learn C, visually

🚀 Modern C (C11 → C23)

static_assert: catch bugs before the program even exists

⏱ 10 min · free interactive lesson · quizzes, visualizations & a real compiler

▶ Open the interactive lesson — free, no signup
Why you're learning this

Plenty of crashes in shipped software are just assumptions nobody ever wrote down — "an int is 4 bytes here", "this table has an entry for every enum value". Today you learn the one-line habit that makes the compiler refuse to build your program until an assumption actually holds. A bug caught that way can never reach anyone's machine, because the broken binary is never created at all.

You already know assert() from debugging: it checks a condition while the program runs and aborts if it's false. But some bugs can be caught much, much earlier — before an executable is even produced. That's what C11's _Static_assert is for: an assertion the compiler checks, at compile time, for free.

This spot has an interactive flow widget — open the interactive lesson to play with it.

Compile time vs run time

The difference is enormous. A failed assert() crashes in front of your users, on their machine, at 3 a.m. A failed static_assert refuses to compile on your machine — the broken binary never exists. The trade-off: a static assert can only check things the compiler can compute — integer constant expressions like sizeof, enum values, and arithmetic on literals. It cannot check user input, file contents, or anything known only at runtime.

check.c
#include <assert.h>   /* gives you the static_assert macro (C11/C17) */
#include <stdio.h>

/* Write your assumptions down — at file scope, checked while parsing: */
static_assert(sizeof(int) == 4,  "this code assumes 32-bit int");
static_assert(sizeof(void *) == 8, "this code assumes 64-bit pointers");

/* Classic ABI check: a network packet header must be EXACTLY 8 bytes */
struct packet {
    unsigned char  type;
    unsigned char  flags;
    unsigned short len;
    unsigned int   seq;
};
static_assert(sizeof(struct packet) == 8, "packet layout broke!");

int main(void) {
    printf("all compile-time checks passed before main() existed\n");
    return 0;
}
terminal
$ gcc -std=c17 check.c -o check && ./check
all compile-time checks passed before main() existed
# the asserts cost NOTHING at runtime — they left no trace in the binary

Notice where the asserts live: at file scope, outside any function. That's allowed — a static assert is a declaration, so it can appear at file scope, inside functions, and even inside a struct definition. Nothing runs; the compiler simply evaluates the condition while parsing.

🧠 Checkpoint: Which of these can static_assert check?

  • That a file exists on disk
  • That sizeof(long) == 8
  • That user input is positive
  • That malloc succeeded
Show answer

That sizeof(long) == 8 — Only integer constant expressions — things the compiler can compute without running the program. Files, input, and malloc results exist only at runtime; those need assert() or real error handling.

Watching one fail

Here's the payoff. Suppose your code genuinely assumes long is 8 bytes (say, you pack pointers into longs). Add the assert, and on any platform where that's false — 32-bit Linux, or Windows where long is 4 bytes even in 64-bit builds — the build stops cold with your message:

the same file on 32-bit / Windows
$ gcc -std=c17 check.c -o check
check.c:6:1: error: static assertion failed: "this code assumes 64-bit pointers"
    6 | static_assert(sizeof(void *) == 8, "this code assumes 64-bit pointers");
      | ^~~~~~~~~~~~~
# build stops. No binary. The bug is caught at the earliest possible moment.
💡

Rule of thumb: every time you catch yourself assuming something about sizes, layout, or ranges ("an int is 4 bytes here", "this struct matches the wire format"), write the assumption down as a static_assert. Assumptions rot; asserts don't.

Three spellings, one feature

standardhow you write it
C11 / C17_Static_assert(expr, "message") — the keyword; <assert.h> adds the nicer macro static_assert
C23static_assert is a real keyword (no header needed), and the message is optional: static_assert(sizeof(int) == 4);

Why the ugly _Static_assert spelling first? Backwards compatibility: names starting with underscore + capital are reserved, so old code that happened to define its own static_assert couldn't break. C23 finally promoted the pretty name once the world had caught up. You'll see the same _Uglypretty pattern all through this part.

🧠 Checkpoint: What did C23 change about static asserts?

  • They can now check runtime values
  • static_assert became a keyword and the message became optional
  • They were removed in favor of assert()
  • They now abort at runtime instead
Show answer

static_assert became a keyword and the message became optional — C23 promoted static_assert from an assert.h macro to a true keyword, and made the second argument optional: static_assert(sizeof(int) == 4); is now legal.

Real-world use: keeping tables in sync

A classic maintenance bug: an enum grows, but a parallel array of names doesn't. Six months later someone indexes past the end. A static assert turns that silent landmine into an instant compile error:

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

enum op { OP_ADD, OP_SUB, OP_MUL, OP_COUNT };   /* _COUNT sentinel trick */

static const char *op_names[] = { "add", "sub", "mul" };

/* If the enum and the table ever disagree, the BUILD breaks — not prod: */
static_assert(sizeof(op_names) / sizeof(op_names[0]) == OP_COUNT,
              "op_names[] out of sync with enum op");

int main(void) {
    for (int i = 0; i < OP_COUNT; i++)
        printf("op %d = %s\n", i, op_names[i]);
    return 0;
}

Add OP_DIV to the enum and forget the string? The build fails with "op_names[] out of sync" — pointing you at the exact fix. This pattern (a _COUNT sentinel plus a static assert) is everywhere in production C.

This spot has an interactive editor widget — open the interactive lesson to play with it.

🧠 Checkpoint: Why must static_assert messages help future readers, e.g. "packet must match wire format"?

  • The message is printed every run
  • The message becomes the compile error someone sees years later
  • The linker requires unique messages
  • Messages are mandatory in C23
Show answer

The message becomes the compile error someone sees years later — When the assert finally fires — often on a new platform, years later — your message IS the diagnostic. "Assertion failed: 1 == 2" helps nobody; a sentence explaining the assumption fixes the bug in minutes. (And C23 actually made messages optional, not mandatory.)

Next up: another thing the compiler knows at compile time — how your data must be aligned in memory, and how to query and control it.

▶ Practice this lesson interactively (with live gcc)