Error Handling & Robustness

defensive copying and validating arguments

Imagine a bank teller who, when handed a cheque, first checks the signature and amount before doing anything, and keeps the bank's own copy of the record rather than relying on the customer's slip which the customer could alter afterwards. Defensive copying and validating arguments are the programming versions of these two habits: do not trust what comes in, and do not depend on data someone else can change behind your back.

Validating arguments means a function checks its inputs against its preconditions before using them: is this pointer non-NULL, is this length within bounds, is this index in range, is this enum a value I actually handle? In C this matters intensely because the language will not check for you - pass a NULL pointer or an out-of-range index and you get a crash or silent corruption, not a clean error. So a robust function inspects its arguments at the top and rejects bad ones with a clear error return, especially at a trust boundary where the data came from outside (a user, a file, the network). Defensive copying addresses a subtler hazard: when a function is handed a pointer to data it will rely on later, and the caller still holds that pointer too, the caller (or another thread) might change or free that data out from under you. Making your own private copy of the data you depend on insulates you from such later mutation - you are reasoning about a snapshot you control, not a moving target. The classic example is storing a caller-supplied string: if you keep just the pointer, the caller may overwrite or free the buffer; if you strdup() your own copy, your data is safe (and now you own that copy and must free it).

Why it matters and the honest balance: these techniques convert vague, far-away crashes into immediate, local, explainable errors caught at the door. They are most valuable precisely at trust boundaries - the seams where data crosses from an untrusted source into your code. The caveat is cost and judgement: validating and copying everything everywhere adds code and runtime overhead, and copying introduces a new ownership question (who frees the copy?). The pragmatic rule is to be strict at the boundary - validate untrusted input thoroughly there, once - and rely on your own enforced invariants (and assertions for bugs) deeper inside, rather than re-checking the same thing in every internal function. And note the distinction from assertions: validating untrusted input is recoverable-error handling that must always run, not a debug-only assert that disappears under NDEBUG.

Validation: int parse(const char *s, size_t n) { if (s == NULL || n == 0) return -1; /* reject bad input at the door */ ... }. Defensive copy: struct config *make(const char *name) { struct config *c = malloc(sizeof *c); c->name = strdup(name); /* our own copy - caller may free theirs */ return c; } - now c->name survives whatever the caller does to its string.

Reject bad arguments at the boundary; copy data you must rely on later.

Validating untrusted input is recoverable-error handling and must always run - do not use a debug-only assert() for it, since that vanishes under NDEBUG. And a defensive copy creates a new owner: whoever made the copy must free it, or you have just traded a sharing bug for a leak.

Also called
argument validationinput checking at the boundary防禦性程式設計