Proven C Book한국어 GitHub

51 Errors and contracts

What to know first

chapter 33, The meaning of a function · how a function reports failure
chapter 43, Safe input · five disciplines for handling failure

Looking back

At the end of chapter 33 the fact function was said to stand on an “implicit promise” (n at least 0; overflow at 13 or above). But that promise was written nowhere in the code — does it then exist?

A. It exists, but in an unkept state — and that is the heart of the problem. Every function has an implicit contract: for what inputs it works properly (preconditions), and what it guarantees on success (postconditions). If that contract is in neither documentation nor code, a violation passes silently and goes off later somewhere unrelated. This chapter is the story of ways to make the contract visible.

The need for this chapter, and its context

The seed of the contract planted in chapter 33 and the five disciplines built in chapter 43 come together here as one principle. That it sits immediately before chapter 52 matters: you must settle what a contract is before you can say what happens when one is broken. The two chapters are a pair.

By the end of this chapter

The seed of the contract planted in chapter 33 grows — what a function demands and what it promises (preconditions and postconditions), how failure is reported (C’s way: errors are values), and the devices that force that value to be checked. The five disciplines set out in chapter 43 are organised here into principles.

The questions this chapter answers

  1. How do chapter 43′s five disciplines connect to this principle?

51.1 Errors are values — C’s way

Many modern languages have a separate channel for failure (exceptions); C has none. In C failure is reported through the return value — the function’s result itself carries “did it succeed?” out with it. There are three conventions.

We see the first in a demonstration. It is the fact learned in chapter 28, that division by zero is outside the contract, governed by a function’s contract.

examples-en/ch51/errval.c

#include <stdio.h>
#include <stdlib.h>

/* The contract: the divisor must not be 0. Failure is reported by the return value. */
[[nodiscard]] bool safe_div(int a, int b, int *out)
{
    if (b == 0) {
        return false;           /* failure — out is left untouched */
    }
    *out = a / b;
    return true;
}

int main(void)
{
    int result = 0;

    if (safe_div(7, 2, &result)) {
        printf("7 / 2 = %d\n", result);
    } else {
        printf("7 / 2: failed\n");
    }

    if (safe_div(7, 0, &result)) {
        printf("7 / 0 = %d\n", result);
    } else {
        printf("7 / 0: cannot divide by zero (reported as a value)\n");
    }
    return 0;
}

Output

7 / 2 = 3
7 / 0: cannot divide by zero (reported as a value)

Three things to read. The contract is written in comments — what is demanded stands beside the code. On failure the output argument is not touched — “nothing is changed on failure” is part of the contract too. And [[nodiscard]] — the C23 notation by which the compiler warns if a call discards this return value. It is a brake on the freedom of chapter 21′s “it is legal to discard a return value”, saying “this one value must not be discarded.” That is exactly why chapter 43′s parsing function wears this notation.

A common misconception. “Error handling is an incidental chore that makes code untidy”

A common impression for a beginner, and C’s error handling really is conspicuously verbose — an if attaches to every call. But invert the perspective and it is exactly the opposite: the error path is half of the program. That a file may be missing, memory may run short, input may be nonsense, is not an exceptional situation but ordinary reality. The evidence is that the overwhelmingly common cause in real accident analyses is “the return value was not checked” — code written only for the success path is code half written. How to reduce the verbosity (gathering into a common cleanup point, using a type that wraps failure) is a matter of technique; the principle of checking is not a matter of compromise.

51.2 The contract in code — assert and defence

There is a tool for writing a precondition as code rather than documentation — assert(condition) of <assert.h>. If the condition is false it stops the program at once and reports the location. Distinguishing its use exactly is important:

This distinction is the contract’s two faces — the inside (invariants I must keep) with assert, the outside (promises the other party may not keep) with checks and error values.

51.3 const — the cheapest contract

There is one more tool for writing a contract in code. It is const, introduced in chapter 23 as “documentation saying I will not change this” and used in chapter 46 as the mark “this function does not touch the original.” Seen again from this chapter’s perspective, const is the contract clause that can be written most cheaply — adding one word in one place in a function signature promises the caller “your data is safe” and hands the compiler the job of watching over that promise.

Its effect spans three layers.

① For people — the burden of reading falls. The moment you see the signature void render(const struct scene *s), it is settled that this function does not change scene. Not having to read the function’s body — in a large codebase there is scarcely a more valuable saving. It is the substance of chapter 23′s “the more of a piece of code that does not vary, the easier it is to read.”

② For the compiler — it becomes grounds for optimisation. Chapter 13 showed the editor holding a value in a register, and the key to that judgement was “can this value change in the meantime?”. const is a signal helping that judgement — though it must be stated exactly: const is not itself a magic optimisation switch. Data arriving through a pointer may still be changed by another route (aliasing), so const alone does not let the compiler be certain of everything. The definite gain is on the side of objects actually declared const (global constants, static const tables) — the compiler may plant the value directly in the code (constant propagation) or place it in a read-only region, making it unmodifiable outright (chapter 42′s string literals lived in that place).

③ For the layers of memory — sharing becomes safe. Data that does not change has no reason to be copied. Many places may read the same thing together, and from chapter 11′s cache perspective several cores may share and read the same cache line without any contention — because the false sharing of chapter 12 is an accident that requires writing. That is why the practice of passing large data as a const pointer instead of by value (chapter 46) is both safe and fast.

So modern practice is simple — make const the default and release only what must change. Widening a contract costs; narrowing it takes one word.

Q. How do chapter 43′s five disciplines connect to this principle?

A. Three of them follow this chapter’s principles exactly. First, failure appears in the type — a bundle that keeps success and value apart is returned, so writing code that “takes the value out without asking whether it succeeded” becomes awkward instead. Second, it forces the check with [[nodiscard]] — discard it and the compiler warns. Third, it takes boundaries and sizes as part of the contract — blocking, at the API level, the root of the boundary violations seen in chapters 37–42. In summary: a design that writes the contract not in documentation but in types and signatures, implementing inside C the same direction as the concerns of Rust and Zig seen in chapter 1. Part XII pushes that design all the way through in a single library.

We can handle contracts and errors. But there remains a world this book has kept deferring under the names “outside the contract” and “undefined behaviour”. The next chapter faces that world head on — the most misunderstood and most expensive subject in C.