Table of Contents
KEY TAKEAWAYS
- Validate inputs at module boundaries — trust nothing from outside
- Use
assert()for programming errors and runtime checks for operational errors - Always use bounded string and buffer operations (
snprintf,strncpy) - Return error codes so callers can handle failures gracefully
- Defensive programming is not paranoia — it is professionalism
What Is Defensive Programming?
Defensive programming is a practice where you write code that anticipates and handles errors, invalid inputs, and unexpected conditions — even when you think they “should never happen.” The goal is to make your code fail safely and predictably rather than silently corrupting data or crashing.In C, this is especially important because the language provides no automatic bounds checking, null safety, or exception handling. If your code does not defend itself, bad things happen silently.Example 1: Function Parameter Validation
Bad — Blind Trust
float calculate_average(const int *values, int count) {
int sum = 0;
for (int i = 0; i < count; i++) {
sum += values[i];
}
return (float)sum / count; // Division by zero if count == 0
// Segfault if values == NULL
}This function trusts the caller completely. If values is NULL or count is 0 or negative, the behavior is undefined — crash, garbage data, or silent corruption.Good — Validate at the Boundary
#include <assert.h>
float calculate_average(const int *values, int count) {
// Assertions for programming errors (debug builds)
assert(values != NULL);
assert(count > 0);
// Runtime guard for safety
if (values == NULL || count <= 0) {
return 0.0f;
}
int sum = 0;
for (int i = 0; i < count; i++) {
sum += values[i];
}
return (float)sum / count;
}assert() catches programming errors during development. The runtime guards handle cases that might occur in production. This dual approach gives you fast feedback during testing and safe behavior in the field.Example 2: Buffer Overflow Prevention
Bad — No Bounds Checking
void build_message(char *buf, const char *name, int value) {
sprintf(buf, "Sensor %s = %d mV", name, value);
// If name is long, this overflows buf
}
void parse_command(const char *input) {
char cmd[16];
strcpy(cmd, input); // If input > 15 chars, stack overflow
}Good — Always Use Bounded Operations
int build_message(char *buf, int buf_size, const char *name, int value) {
int written = snprintf(buf, buf_size, "Sensor %s = %d mV", name, value);
if (written >= buf_size) {
// Truncated — caller can detect this
return -1;
}
return written;
}
bool parse_command(const char *input, char *cmd, int cmd_size) {
if (strlen(input) >= (size_t)cmd_size) {
return false; // Input too long
}
strncpy(cmd, input, cmd_size - 1);
cmd[cmd_size - 1] = '';
return true;
}Always pass buffer sizes. Always use snprintf instead of sprintf, strncpy instead of strcpy. Report truncation so the caller can handle it.Defensive Programming Techniques
1. Assert for Programming Errors
void motor_set_speed(Motor *m, int rpm) {
assert(m != NULL);
assert(rpm >= 0 && rpm target_rpm = rpm;
}2. Return Error Codes
typedef enum { OK, ERR_NULL, ERR_RANGE, ERR_TIMEOUT } Result;
Result sensor_read(Sensor *s, float *out) {
if (s == NULL || out == NULL) return ERR_NULL;
if (!sensor_is_ready(s)) return ERR_TIMEOUT;
*out = s->read_func(s->ctx);
if (*out min || *out > s->max) return ERR_RANGE;
return OK;
}3. Sentinel Values and Safe Defaults
#define INVALID_TEMP (-999.0f)
float read_temperature(void) {
int raw = adc_read(TEMP_CHANNEL);
if (raw 4095) {
return INVALID_TEMP; // Known bad value, easy to check
}
return raw * 0.1f - 40.0f;
}4. Limit Array and Loop Bounds
// Always clamp indices
int safe_get(const int *arr, int size, int index) {
if (index = size) {
return 0; // safe default
}
return arr[index];
}
// Always limit loop iterations (avoid infinite loops)
bool wait_for_ready(int max_attempts) {
for (int i = 0; i < max_attempts; i++) {
if (device_is_ready()) return true;
delay_ms(1);
}
return false; // Timed out
}When to Be Defensive
- Public API boundaries — any function callable from outside your module should validate inputs
- Hardware interfaces — sensors and peripherals can return garbage data
- User input and external data — network packets, serial commands, file contents
- Memory allocation — always check
mallocreturn values
When NOT to Be Defensive
- Internal helper functions — if a static function is only called from verified paths, heavy validation adds noise
- Performance-critical inner loops — validate before the loop, not inside it
- Already-validated data — if the input was checked at the boundary, downstream functions can trust it
Key Takeaways
- Validate inputs at module boundaries — trust nothing from outside
- Use
assert()for programming errors and runtime checks for operational errors - Always use bounded string and buffer operations (
snprintf,strncpy) - Return error codes so callers can handle failures gracefully
- Defensive programming is not paranoia — it is professionalism

Vivek Bhageria — Lead Firmware R&D Engineer, 12+ years. Ex-Bosch (automotive powertrain), MusicTribe (real-time audio), medical devices. M.Tech BITS Pilani. I write at NerdyElectronics — practical, register-level embedded systems for engineers who want to understand what’s actually happening under the hood.







