Posted in

When Code Hits the Fan: Defensive Programming #2

If you have ever stared at a segmentation fault like it was an abstract painting, beautiful, mysterious, and slightly insulting, then congratulations: you have experienced the deep joy of debugging C.

Fortunately, the language gives us a few tools to keep the madness under control.

Let’s talk about three of them: assert, static_assert, and logging.

What is assert in C?

assert is a macro defined in <assert.h> that lets you verify at runtime that a certain condition is true.
If the condition turns out to be false, the program:

  1. prints an error message to stderr, including:
    • the file name,
    • the line number,
    • the failed expression,
  2. and then terminates by calling abort().

Think of it as a debugging landmine: you plant it, and if the program steps on it, it blows up immediately before something worse happens. It’s a development tool, not an error-handling mechanism for end users.

#include <assert.h>

assert(condition);
  • If condition is true → nothing happens; execution continues normally.
  • If condition is false → a message is printed and the program terminates.

Example

int x = 5;
assert(x > 0);      // OK
assert(x == 10);    // fails -> abort()

So it can be used to:

  • verifying invariants,
  • checking preconditions and postconditions,
  • catching bugs as early (and as loudly) as possible.

It helps catch conditions that should never occur in a correctly functioning program unless reality has gone off-script again.

Asserts MUST be disabled entirely in production builds. When disabled, they don’t cost a single CPU cycle.

The Error Message Generated by assert

It typically looks like this:

Assertion failed: x == 10, file main.c, line 7
Aborted (core dumped)

Clear, loud, and mildly accusatory. Exactly what you want from a debugging tool.

How to Disable Asserts (Using NDEBUG)

You can disable assertions by compiling with the macro symbol NDEBUG defined.

Compiler option:

gcc -DNDEBUG main.c -o main

Or directly in code:

#define NDEBUG
#include <assert.h>

When Not to Use assert

assert should not be used to handle normal or expected runtime errors.
It is a bug-hunting weapon, not a polite input-validation routine.

Do not use it for:

  • validating user input
  • checking files, sockets, network data, or command-line parameters
  • handling system errors

That’s because the program will shut down on the user.

In these cases, use soft error-handling tools:

  • if + error messages,
  • return values,
  • simulated exceptions,
  • errno, etc.

But this will be the subject of another article.


Caveat

Mind the Context

When NDEBUG is defined, the expression inside assert() is not even evaluated.

So something like:

assert(x++);

is a terrible idea, because in release mode x would never be incremented.
If your logic depends on it, you’ve already lost.

Not for High-Reliability Code

Since asserts terminate the program, they are generally avoided. It should be used only for debugging. Please, please don’t use it in production, use proper fault-tolerant mechanisms instead.

Zero Cost in Release

With NDEBUG, asserts disappear completely like they were never written.

Custom Asserts

Sometimes developers define their own:

#define my_assert(e, msg) \
    ((e) ? (void)0 : (fprintf(stderr, "Error: %s\n", msg), abort()))


Meet static_assert: The Compiler’s Way of Saying “Nope.”

If assert() is the loud, dramatic friend who only complains at runtime, static_assert() is the quiet, strict librarian who stops you at compile time and refuses to let you proceed until you fix your mistakes. It’s the C11 gift we didn’t know we needed: a way to check assumptions before your program even thinks about running.

static_assert lets you encode facts the compiler must agree with: things like structure sizes, type properties, magic constants, or the assumption that int is 32 bits. If the condition is false, the compiler throws an error with your custom message and slams the door shut. No executable. No undefined behavior. No negotiations.

In practice, this turns your build process into a safety net woven from pure logic. You’re telling the compiler:
“If this condition is wrong, don’t let me ship this nonsense.”

And the compiler obliges sternly, mercilessly, and well before your code can cause mayhem at runtime.

It’s like having a time-traveling bug detector that catches problems while they’re still just bad ideas in your head.

Checking Type Sizes

Making sure the code runs only on platforms where assumptions hold.

#include <assert.h>
static_assert(sizeof(int) == 4, "This code requires 32-bit ints!");

If someone tries to compile this on a system where int is 16 or 64 bits, the compiler throws a fit—exactly as intended.

Validating Structure Layout

Ensuring a struct has the expected binary footprint.

struct PacketHeader {
    uint8_t  type;
    uint8_t  flags;
    uint16_t length;
};

static_assert(sizeof(struct PacketHeader) == 4,
              "PacketHeader must be exactly 4 bytes!");

If padding sneaks in unexpectedly, the build goes boom.

Keeping Magic Numbers Honest

Confirming constants really are what you think they are (or what past-you promised they’d be).

enum { MAX_USERS = 128 };

static_assert(MAX_USERS % 2 == 0,
              "MAX_USERS must be even (because reasons).");

Great for catching “harmless” refactors that break invariant assumptions.

Array Size Verification

Ensuring an array has the expected number of elements.

int primes[] = {2, 3, 5, 7, 11};

static_assert(sizeof(primes)/sizeof(primes[0]) == 5,
              "List of primes must have exactly 5 elements!");

Perfect for preventing off-by-one surprises.

API Contract Enforcement

Verifying relationships between compile-time constants.

#define BUFFER_SIZE 256
#define CHUNK_SIZE  64

static_assert(BUFFER_SIZE % CHUNK_SIZE == 0,
              "BUFFER_SIZE must be divisible by CHUNK_SIZE!");

This stops accidental configuration mismatches before they can ruin your day.


assert vs static_assert

When the Check Happens

assert

  • Evaluated at runtime, while the program is actually running and (hopefully) behaving.
  • If the condition is false then it prints a message and calls abort(), throwing the program off a cliff immediately.

static_assert

  • Evaluated at compile time, long before your code even becomes a real executable.
  • If the condition is false then you get a compile-time error, and the program doesn’t build at all. Not even a chance to misbehave.

Required Header

assert

#include <assert.h>

static_assert

  • Since C11:
		#include <assert.h>   // static_assert is defined here
  • Or using the keyword form:
		static_assert(condition, "message");

In C11, static_assert is also a keyword, so in many contexts it can be used without including any header—because it’s part of the language itself.

Type of Verification

assert

Checks logical runtime invariants such as:

  • non-empty arrays,
  • non-null pointers,
  • preconditions and postconditions,
  • anything that should be true during program execution.

static_assert

Checks conditions that must be true at compile time, like:

  • type sizes (sizeof),
  • configuration constants,
  • macro values,
  • structure compatibility,
  • compile-time-known constants.

Example:

static_assert(sizeof(void*) == 8, "Requires a 64-bit environment");

If that’s not true, the compiler shuts down the party immediately.

Behavior in Release Mode (NDEBUG)

assert

If you define NDEBUG, all assert() calls disappear:

#define NDEBUG
#include <assert.h>

The expression isn’t even evaluated.

static_assert

Cannot be disabled. Ever.
It’s permanently enforced as part of the compiler’s sanity checks.

Effect on Generated Code

assert

  • Introduces overhead only in debug builds.
  • In release mode, it vanishes like a ninja.

static_assert

  • Introduces zero runtime overhead.
  • Generates no code whatsoever, because it acts purely at compile time.

In practice, use assert when:

  • You’re checking conditions that depend on runtime data.
  • You’re hunting down logic bugs and need something to yell when reality contradicts your assumptions.
  • You’re validating preconditions and invariants during execution.

Use static_assert when:

  • You want to guarantee certain properties of the program before it ever runs.
  • You’re checking constants, types, or configuration values that must be correct at compile time.
  • You need to stop the build cold if someone sets things up in an invalid or dangerous way.

Let’s combine assert and static_assert with a practical example:


Validating Structural Invariants of a System

This technique is common in embedded systems and other environments where mistakes tend to explode in wonderfully catastrophic ways.

static_assert: checks the structure itself

typedef struct {
    uint16_t header;
    uint16_t length;
    uint8_t  payload[256];
} Packet;

static_assert(sizeof(Packet) == 260,
              "Packet size mismatch: potential network overflow ahead!");

Here, static_assertguarantees that the Packet layout is exactly what we expect at compile time, no secret padding, no ABI surprises, no “oops, why is this struct suddenly 264 bytes?”

assert: checks the runtime contents

void packet_send(Packet *p) {
    assert(p != NULL);
    assert(p->length <= sizeof(p->payload));
    ...
}

These runtime assertions ensure that the actual data inside the packet is valid when the program is running, catching logic errors before they turn into corrupted frames or mysterious firmware crashes.


Logs in C AKA how do not “printf” Everywhere


Real programs deserve real logs. So let’s build a clean logging system that shoots messages both to your console and to a file.

A grown-up logging system should support:

  • Multiple destinations: console, file, or both.
  • Log levels: INFO, WARN, ERROR, DEBUG, your standard emotional spectrum of a C developer.
  • Timestamps: so you know when your app decided to misbehave.
  • One unified API so you don’t end up copy-pasting printf formats like a cryptic ritual.


The Header

C
// logger.h</code>
#ifndef LOGGER_H</code>
#define LOGGER_H</code>

#include <stdio.h></code>

typedef enum {
  LOG_INFO,
  LOG_WARN,
  LOG_ERROR,
  LOG_DEBUG
  } LogLevel;

void log_init(const char *filename);
void log_close();
void log_message(LogLevel level, const char *fmt, ...);

#endif


Implementing the Logger

C
// logger.c
#include "logger.h"
#include <stdarg.h>
#include <time.h>

static FILE *log_file = NULL;

void log_init(const char *filename) {
    log_file = fopen(filename, "a");
}

void log_close() {
    if (log_file) fclose(log_file);
}

static const char* level_to_string(LogLevel level) {
    switch(level) {
        case LOG_INFO:  return "INFO";
        case LOG_WARN:  return "WARN";
        case LOG_ERROR: return "ERROR";
        case LOG_DEBUG: return "DEBUG";
        default:        return "UNKNOWN";
    }
}

void log_message(LogLevel level, const char *fmt, ...) {
    time_t t = time(NULL);
    struct tm *tm_info = localtime(&t);

    char timebuf[20];
    strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S", tm_info);

    va_list args;
    va_start(args, fmt);

    // Console output
    printf("[%s] [%s] ", timebuf, level_to_string(level));
    vprintf(fmt, args);
    printf("\n");

    // File output
    if (log_file) {
        fprintf(log_file, "[%s] [%s] ", timebuf, level_to_string(level));
        vfprintf(log_file, fmt, args);
        fprintf(log_file, "\n");
        fflush(log_file);
    }

    va_end(args);
}

And an example on how to use it:

C
#include "logger.h"

int main() {
    log_init("app.log");

    log_message(LOG_INFO, "Application started");
    log_message(LOG_DEBUG, "Debugging value: %d", 69);
    log_message(LOG_WARN, "Memory is getting suspiciously low");
    log_message(LOG_ERROR, "Something exploded internally!");

    log_close();
    return 0;
}


Voilà ! Now your program reports its emotional state in real time.


Now With Colors, Because Your Console Deserves Some Style

C
// logger.c
#include "logger.h"
#include <stdarg.h>
#include <time.h>

// ANSI color escape codes
#define COLOR_RESET   "\033[0m"
#define COLOR_INFO    "\033[32m"  // green
#define COLOR_WARN    "\033[33m"  // yellow
#define COLOR_ERROR   "\033[31m"  // red
#define COLOR_DEBUG   "\033[34m"  // blue

static FILE *log_file = NULL;

void log_init(const char *filename) {
    log_file = fopen(filename, "a");
}

void log_close() {
    if (log_file) fclose(log_file);
}

static const char* level_to_string(LogLevel level) {
    switch(level) {
        case LOG_INFO:  return "INFO";
        case LOG_WARN:  return "WARN";
        case LOG_ERROR: return "ERROR";
        case LOG_DEBUG: return "DEBUG";
        default:        return "UNKNOWN";
    }
}

static const char* level_to_color(LogLevel level) {
    switch(level) {
        case LOG_INFO:  return COLOR_INFO;
        case LOG_WARN:  return COLOR_WARN;
        case LOG_ERROR: return COLOR_ERROR;
        case LOG_DEBUG: return COLOR_DEBUG;
        default:        return COLOR_RESET;
    }
}

void log_message(LogLevel level, const char *fmt, ...) {
    time_t t = time(NULL);
    struct tm *tm_info = localtime(&t);

    char timebuf[20];
    strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S", tm_info);

    va_list args;
    va_start(args, fmt);

    // ---- Console output (with colors!) ----
    printf("%s[%s] [%s] ", level_to_color(level), timebuf, level_to_string(level));
    vprintf(fmt, args);
    printf("%s\n", COLOR_RESET);

    // ---- File output (no color codes!) ----
    if (log_file) {
        fprintf(log_file, "[%s] [%s] ", timebuf, level_to_string(level));
        vfprintf(log_file, fmt, args);
        fprintf(log_file, "\n");
        fflush(log_file);
    }

    va_end(args);
}


Your console is now officially a rainbow of diagnostics.

Caveat

  • Thread safety: If your app uses multiple threads, wrap logging calls in a mutex so logs don’t collide like hungry threads racing for malloc.
  • Log rotation: Unless you want a 9-GB log file rotate or compress old logs.
  • Debug toggles: Make debug logs optional at compile time.
  • Keep logs short inside tight loops: Unless you want your app to run at “potato” speed.

Leave a Reply

Your email address will not be published. Required fields are marked *