JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Return Codes, errno, and Checking Every Call

Every call you make to the system can fail, and C will never stop you from ignoring that. This guide shows the two channels C actually uses to report failure — the return value and errno — and why the humble habit of checking every single call is the foundation the whole rung is built on.

C has no safety net — failure is just a value

You have spent the earlier rungs learning how a program actually runs: how it asks the kernel to open files with a system call, how malloc() carves memory off the heap, how a process talks to the world through file descriptors. Every one of those requests can be refused. The disk is full, the file does not exist, you are out of memory, the network dropped. The crucial fact this rung is built on is that C, by design, has no built-in machinery that forces you to notice. There are no exceptions thrown, no automatic unwinding, no runtime that halts you. If a call fails and you do not look, your program simply marches on with a wrong value in its hands.

This makes error handling in C a first-class concern rather than an afterthought: because the language will not do it for you, you must do it explicitly, at every call, with ordinary code. That sounds like a burden, and at first it is — but it is also a kind of honesty. In C, handling errors is part of writing the program, not a decoration added later. A function that does real work and a function that handles its failures are not two programs; they are the same program written properly. Treat the failure path as first-class code from the very first line you write, and the rest of this rung — cleanup, assertions, graceful degradation — has somewhere solid to stand.

Two channels: the return value and errno

When a C library or system call fails, it has to tell you two different things, and it uses two different channels for them. The first channel is the return value: a single value, in band, that says *did this succeed or fail?* The second channel is errno, a global integer the library sets to say *if it failed, why?* Keeping these straight is the heart of this guide, because they answer different questions and you read them at different moments. The return value is your gate; errno is the explanation you fetch only after the gate tells you something went wrong.

The return value is a sentinel: one specially chosen value, picked out of the normal range, that means "failure." Each call documents its own sentinel, and they genuinely differ. open() returns a file descriptor on success, but -1 on failure. malloc() returns a pointer to your block, but NULL on failure. read() returns the number of bytes read, but -1 on error. fork() returns a pid, but -1 on failure. There is no single universal convention — and that is precisely why you must read the manual page for each call and learn its specific sentinel, rather than guessing.

Now the subtle part, and the single most common errno mistake. errno is a global that gets overwritten by the next call that fails, and many successful calls leave it untouched at whatever it was. So errno only carries meaning at one precise instant: immediately after a call has told you, through its return value, that it failed. The rule is exact: first check the return value; only if it signals failure do you read errno; and read it before you make any other library call, because that next call may clobber it. Reading errno when the return value said success is meaningless — it may hold a stale code from minutes ago.

the two channels, read in the right order:

    int fd = open("data.txt", O_RDONLY);
    if (fd == -1) {              /* channel 1: the sentinel says FAIL */
        int e = errno;          /* channel 2: read errno NOW, save it */
        fprintf(stderr, "open: %s\n", strerror(e));
        /* strerror(e) turns e.g. ENOENT into "No such file or directory" */
        return -1;
    }
    /* fd is valid here, and ONLY here */

perror("open") does the same as the strerror line above, in one call.
Return value first (the gate), errno second (the reason), turned into human text with strerror() or perror().

Why "check every call" is not paranoia

It is tempting to check only the calls you think are risky and wave the rest through. This is a trap, and it has a name: the cost of unchecked errors. The danger is not that an unchecked call crashes immediately — if only it did. The real danger is that it returns a plausible-looking wrong value that your program happily uses, and the corruption surfaces far away, long after, in code that is completely innocent. An open() you did not check returns -1; you treat -1 as a file descriptor and read() from it; that read also fails; you process the empty buffer as if it were data — and three functions later something prints garbage, with no hint of where it began.

This is why the discipline is to check every call that can fail, not just the scary ones. The classic example everyone should burn into muscle memory is checking malloc: malloc() returns NULL when it cannot give you memory, and if you skip the check and dereference that NULL, you get an immediate segmentation fault in the lucky case — and silent heap corruption in the unlucky one. The check is two lines and it converts an invisible time bomb into a clean, located failure. The asymmetry is overwhelming: checking costs you a branch; not checking costs you a debugging session, or a security hole, at an unknowable later time.

Library calls, system calls, and where errno comes from

It helps to know who sets errno, because it clarifies which calls play by these rules. Recall from the OS rung that a call like open() or read() is not an ordinary function call — it is a thin C wrapper around a real system call that crosses into the kernel. The kernel itself reports failure through a small negative number in a register; the libc wrapper catches that, sets errno to the corresponding positive code, and hands you back -1. So errno is the libc-and-POSIX convention for surfacing kernel errors into C, and it is shared by the whole family of POSIX calls — open(), read(), write(), close(), fork(), and their kin.

Not every function uses errno, though, and assuming it does is a quiet bug. Pure C-library functions that do not touch the kernel often report failure through the return value alone — strtol() signals overflow, and some math functions are the rare ones that do set errno. Higher-level functions sometimes invent their own scheme entirely: returning a small enum of error codes, or 0 for success and nonzero for an error code. The practical takeaway is to read each function's documentation and answer three questions before you call it: what is its sentinel, does it set errno, and what does it leave behind on failure? Never assume; the conventions are a patchwork.

There is also a small thread-safety point worth carrying forward. On any modern system errno is not really one shared global — it is per-thread, so a failure in one thread does not scramble another's error code. You do not need to do anything special to get this; the system header arranges it. But it is one more reason the timing rule holds: read errno right after the failing call, in the same thread, before anything else runs, and it will tell you the truth.

After you detect a failure: handle, propagate, or abort

Detecting an error is only half the job; the other half is deciding what to do about it. There are exactly three honest responses, and naming them keeps your code clear: handle, propagate, or abort. Handle it means you can recover right here — the file was missing, so fall back to a default; the connection dropped, so retry. Propagate it means this function cannot decide, so it returns its own failure sentinel to its caller and lets a layer that has more context choose. Abort means the error reflects a broken assumption so fundamental that continuing is unsafe, so you log and stop.

Propagation is what turns a pile of checked calls into a robust program, and it is the bridge to several later guides. A low-level function detects a failure, returns -1; its caller sees that -1, does any cleanup it owns, and returns -1 in turn; and so the failure climbs the call stack until it reaches a layer that can meaningfully react — print a message to the user, retry, or exit with a nonzero exit status. That deliberate handing-up of errors is error propagation, and you will meet it again squarely in the guide "Recoverable Errors vs Bugs," which sharpens which of handle, propagate, or abort each situation deserves.

But propagation creates a problem this guide must hand off, not solve: when a function returns -1 halfway through, what about the file it already opened and the memory it already allocated? Returning early without closing them leaks resources. That is exactly the subject of the very next guide, "Cleaning Up: goto-Cleanup and RAII," where the propagate path learns to unwind its own resources cleanly on the way out. For now, the foundation is set: detect failure on every call through the return value, read errno when and only when it is meaningful, and consciously choose to handle, propagate, or abort. Build on that, and every robustness technique in this rung has a place to attach.