Linux System Programming · intermediate · ~8 min
By the end of this lesson you will be able to: - Recognize how POSIX functions signal failure (a sentinel return value such as `-1` or `NULL`) versus how they explain failure (the `errno` variable). - Read `errno` correctly: check the return value *first*, then read `errno` *immediately*, before any other library call can overwrite it. - Convert a numeric error code into human-readable text with `perror` and `strerror` (and the thread-safe `strerror_r`). - Compare specific error codes (`ENOENT`, `EACCES`, `EINTR`, ...) so your program can react differently to different failures. - Follow the standard C convention in your *own* functions: on error, set `errno` and return a sentinel; on success, leave `errno` alone. - Save `errno` into a local variable when you need its value later, and explain why this matters in real programs.
When you call a function that talks to the operating system — opening a file, reading from a socket, allocating memory — that call can fail for reasons completely outside your control. The disk could be full, the file might not exist, you might lack permission, or another process could have deleted the file a microsecond ago. A robust program does not just hope these calls succeed; it checks every one and reacts sensibly when they do not.
This lesson builds directly on open / read / write / close, where you saw that open returns -1 on failure and read/write return -1 too. That -1 tells you something went wrong, but not what. The missing piece is errno: a small integer the C library and the kernel set to a code describing the reason for the most recent failure. Together, the return value and errno form the standard error-reporting contract of POSIX and the C standard library.
In plain language: the return value answers "did it fail?" and errno answers "why did it fail?". You always read them in that order. Checking errno without first seeing a failing return value is meaningless, because errno is only guaranteed to be set when a function actually reports an error.
The terminology you will meet here:
errno — an int-valued expression (declared in <errno.h>) that holds the last error code. On modern systems it is thread-local: each thread has its own copy.ENOENT ("No such file or directory") and EACCES ("Permission denied"). Always compare against these names, never against raw numbers, because the numbers differ between systems.-1, NULL, sometimes (size_t)-1) a function uses to mean "I failed; look at errno."perror / strerror — helpers that translate an errno code into a readable message.Almost every real-world bug report that starts with "it just doesn't work" comes down to an unchecked error. A program that ignores a failed open will happily read from file descriptor -1, get more failures, and eventually crash or silently corrupt data — with no clue about the original cause. Reading and reporting errno turns a baffling crash into a one-line, actionable message: open config.txt: Permission denied.
Good error reporting is also a reliability and security property. A server that distinguishes ENOENT (file missing) from EACCES (permission denied) from EMFILE (too many open files) can respond correctly: create the file, refuse the request, or shed load. A program that treats all failures identically — or, worse, treats failure as success — is exactly the kind of software that loses data, leaks resources, or hangs under pressure.
In professional codebases, consistent errno handling is the difference between logs you can debug at 3 a.m. and logs that tell you nothing. It is one of the most quietly important habits in systems programming.
Definition. Most POSIX and standard-library functions report failure by returning a sentinel value and setting errno to an error code.
How it works. When a system call fails inside the kernel, the C library wrapper stores the kernel's error number into your thread's errno and returns the sentinel (-1, or NULL for pointer-returning functions). Your code must check the return value first; only if it indicates failure is errno meaningful.
your code libc wrapper kernel
-------- ------------ ------
fd = open(...) --> syscall ............> tries to open
fails: EACCES
fd == -1 <-- set errno=EACCES <--- returns error
read errno -----------------------------> EACCES ("Permission denied")
When to use it. Every time you call a function whose documentation says "on error, returns X and sets errno" — which is the vast majority of <stdio.h>, <unistd.h>, <fcntl.h>, and <stdlib.h> I/O and resource functions.
When NOT to rely on it. Some functions (e.g. string functions like strcpy) do not set errno at all. And a successful call may leave errno holding garbage from a previous failure — success does not clear it. So never infer success from errno.
Pitfall. Writing if (errno != 0) to detect failure. errno is not reset to 0 on success, so this gives false positives. Always test the return value.
Knowledge check (concept): A function returns
NULL. Iserrnoguaranteed to describe the reason? (Answer: only if that function's documentation states it setserrnoon failure —fopendoes, but a generic helper might not.)
Definition. errno reflects the most recent error. Any subsequent library call may overwrite it, even a call that succeeds.
How it works. errno is a single per-thread slot. When you call fprintf to print an error message, fprintf itself may set errno (for example to EINTR or 0). If you read errno after that, you may see the wrong code.
TIME ->
open(...) fails .......... errno = ENOENT <-- the value you want
fprintf(stderr, "...") .... errno = (maybe changed!)
strerror(errno) .......... reads the WRONG value
The fix is to capture errno the instant the failing call returns:
int fd = open(path, O_RDONLY);
if (fd < 0) {
int saved = errno; /* snapshot before anything else */
do_some_logging();
fprintf(stderr, "%s\n", strerror(saved));
}
When to use the snapshot. Whenever you do anything between the failing call and your use of errno, or when you must restore errno afterward (common in library code that should not disturb the caller's errno).
Pitfall. Calling perror or strerror is usually fine immediately after the failing call, but inserting even one innocent-looking statement (a printf, a close, a malloc) between them can corrupt the value.
Knowledge check (find-the-bug):
if (read(fd, buf, n) < 0) { close(fd); /* line A */ perror("read failed"); /* line B */ }Why might line B print the wrong reason? (Answer:
closeon line A can itself fail and seterrno, so by line Berrnono longer reflects thereaderror. Saveerrnobeforeclose.)
Definition. perror and strerror map a numeric code to descriptive text.
void perror(const char *s) prints s, then ": ", then the message for the current errno, then a newline — all to stderr.char *strerror(int errnum) returns a pointer to a string for the given code, which you can format yourself.int strerror_r(int errnum, char *buf, size_t buflen) is the thread-safe variant that writes into your buffer (recommended in multithreaded code).How it works internally. Both look up the code in a system message table. strerror may return a pointer to a static buffer that is shared and may be overwritten by the next strerror call — which is why strerror_r exists for threads.
When to use which.
| Need | Use |
|---|---|
| Quick message + your own prefix, single-threaded | perror("context") |
| Embed the message inside a larger formatted string | strerror(errno) |
| Multithreaded code, or you must not risk a shared buffer | strerror_r(...) |
Pitfall. Saving the pointer from strerror and reusing it later. The string it points to may change. Copy it if you need to keep it.
Knowledge check (predict-the-output): If
errno == ENOENTand you callperror("open config.txt"), what is printed tostderr? (Answer:open config.txt: No such file or directory.)
Definition. Error constants such as ENOENT, EACCES, EINTR, EAGAIN, EEXIST, and EINTR name specific failure causes.
How it works. After a failed call you can branch on the code. The classic example is EINTR: a slow system call interrupted by a signal. The correct response is usually to retry, not to abort.
ssize_t n;
do {
n = read(fd, buf, len);
} while (n < 0 && errno == EINTR); /* retry on interruption */
if (n < 0) {
perror("read");
}
When to use it. Whenever different failures deserve different handling: create-if-missing (ENOENT), report-and-stop (EACCES), retry (EINTR/EAGAIN), back off (EMFILE).
Pitfall. Comparing errno to a literal number (e.g. errno == 2). The numeric value of ENOENT is not portable; always use the symbolic name from <errno.h>.
Definition. When you write a function that can fail, follow the same contract: on error, set errno to a meaningful code and return a sentinel; on success, return a non-error value and do not touch errno.
How it works. You may assign one of the standard constants, e.g. errno = EINVAL; for a bad argument or errno = EDOM; for a math-domain error (like dividing by zero in a checked divide). Callers can then use perror/strerror on your function exactly as they would on a libc call.
caller your_function
------ -------------
rc = checked_div(...) --> if (b==0) { errno=EDOM; return -1; }
rc == -1 ? <-- else *out = a/b; return 0;
perror("checked_div")
When to use it. Library-style functions and any helper that wraps failure-prone operations.
Pitfall. Setting errno on the success path. That breaks callers who, correctly, only read errno after seeing your sentinel — but it also pollutes the value for code that (incorrectly but commonly) inspects errno loosely. Leave errno untouched when you succeed.
#include <errno.h> /* declares errno and the E* constants */
#include <string.h> /* strerror, strerror_r */
#include <stdio.h> /* perror, fprintf */
/* errno is an lvalue you can read AND assign: */
errno = 0; /* optional reset before a call that may not set it */
FILE *f = fopen("x", "r");
if (f == NULL) { /* 1. check sentinel first */
int e = errno; /* 2. snapshot immediately */
fprintf(stderr, "fopen: %s\n", strerror(e)); /* 3. translate */
}
/* perror automatically uses the current errno and prints to stderr: */
if (remove("x") != 0)
perror("remove"); /* prints: remove: <reason> */
Key points:
errno may be a macro that expands to a function call (*__errno_location()), so treat it as a normal int lvalue but always include <errno.h>.perror's argument is a context prefix, usually the operation or filename. Pass NULL or "" to print just the message.strerror_r(errnum, buf, sizeof buf) over strerror.Most POSIX functions signal failure in two steps:
-1, or NULL for functions that return a pointer.errno to a numeric code that describes what went wrong.The return value tells you that a call failed. errno tells you why.
Two helpers convert errno into readable text:
perror("context") prints context: <human-readable error> to standard error (stderr).strerror(errno) returns the matching message as a string, which you can print yourself.On modern systems errno is thread-local (each thread has its own copy), so one thread cannot clobber another thread's value.
But within a single thread, the very next library call can overwrite errno. If you need the value later, copy it into your own variable immediately after the failing call.
#include <stdio.h> /* fprintf, perror */
#include <string.h> /* strerror */
#include <errno.h> /* errno, error constants */
#include <fcntl.h> /* open, O_RDONLY */
#include <unistd.h> /* read, close */
#define BUF_SIZE 256
/* Open a file, read up to BUF_SIZE bytes, and print a clear diagnostic
on any failure. Returns 0 on success, 1 on failure. */
int dump_head(const char *path) {
int fd = open(path, O_RDONLY);
if (fd < 0) {
int saved = errno; /* snapshot before any other call */
/* React differently based on the specific cause: */
if (saved == ENOENT) {
fprintf(stderr, "open %s: file does not exist\n", path);
} else if (saved == EACCES) {
fprintf(stderr, "open %s: permission denied\n", path);
} else {
fprintf(stderr, "open %s: %s\n", path, strerror(saved));
}
return 1;
}
char buf[BUF_SIZE];
ssize_t n;
do {
n = read(fd, buf, sizeof buf);
} while (n < 0 && errno == EINTR); /* retry if interrupted by a signal */
if (n < 0) {
int saved = errno; /* save before close() can change it */
close(fd); /* clean up the descriptor regardless */
fprintf(stderr, "read %s: %s\n", path, strerror(saved));
return 1;
}
if (fwrite(buf, 1, (size_t)n, stdout) != (size_t)n) {
perror("fwrite"); /* perror reads current errno */
close(fd);
return 1;
}
if (close(fd) < 0) { /* even close can fail */
perror("close");
return 1;
}
return 0;
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "usage: %s <path>\n", argv[0]);
return 2;
}
return dump_head(argv[1]);
}
What it does. The program takes a filename on the command line, opens it read-only, reads up to 256 bytes, and writes them to standard output. Every system call is checked. On failure it prints a specific, human-readable diagnostic to stderr and returns a non-zero exit status.
Expected output.
notes.txt containing hello:hello
(printed to stdout; exit status 0)./a.out nope.txt prints to stderr:open nope.txt: file does not exist
and exits with status 1.open <path>: permission denied.Edge cases. A read of 0 bytes means end-of-file, not an error (here it simply writes nothing). The EINTR retry loop matters on systems where signals can interrupt blocking calls. close is checked because on some filesystems a deferred write error surfaces only at close.
int fd = open(path, O_RDONLY); — request a read-only file descriptor. On failure open returns -1 and sets errno.if (fd < 0) — the sentinel check. We act only because the return value signaled failure.int saved = errno; — immediately snapshot the code. From here on, any function we call could overwrite the global errno, so we work from saved.if (saved == ENOENT) ... else if (saved == EACCES) ... else strerror(saved) block — branches on the specific cause to give a tailored message, falling back to the system message for anything else.do { n = read(...); } while (n < 0 && errno == EINTR); — read returns the number of bytes read, 0 at EOF, or -1 on error. If the error is EINTR (interrupted by a signal) we retry; any other error breaks the loop.if (n < 0) handles a genuine read error. We snapshot errno before calling close, because close can fail and overwrite it.fwrite(buf, 1, n, stdout) writes exactly n bytes; if the count returned does not match, perror("fwrite") reports why using the current errno (safe here because nothing intervened).close(fd) releases the descriptor; we check it too, since a buffered write error can appear at close time.A trace for ./a.out nope.txt when the file is missing:
| Step | Call | Return | errno | Action |
|---|---|---|---|---|
| 1 | open("nope.txt", O_RDONLY) |
-1 |
ENOENT |
enter failure branch |
| 2 | snapshot | — | saved=ENOENT | saved == ENOENT true |
| 3 | fprintf(stderr, ...) |
— | (may change) | prints "file does not exist" |
| 4 | return 1 |
— | — | main returns 1 |
Mistake 1 — reading errno without checking the return value.
fopen("x", "r");
if (errno != 0) puts("failed"); /* WRONG */
Why it's wrong: errno is not cleared on success, so it may hold a stale code from an earlier failure, producing a false "failed". Recognize it when your program reports errors that never happened.
FILE *f = fopen("x", "r");
if (f == NULL) perror("fopen"); /* RIGHT: check the sentinel */
Mistake 2 — letting another call clobber errno first.
if (open(path, O_RDONLY) < 0) {
printf("trying again...\n"); /* printf may change errno */
perror("open"); /* WRONG: now reports printf's errno */
}
Fix: snapshot before doing anything.
if (open(path, O_RDONLY) < 0) {
int e = errno;
printf("trying again...\n");
fprintf(stderr, "open: %s\n", strerror(e));
}
Recognize it when error messages are oddly generic ("Success", "Invalid argument") for an operation that clearly failed for another reason.
Mistake 3 — comparing errno to a raw number.
if (errno == 2) ... /* WRONG: 2 happens to be ENOENT on Linux, not portable */
if (errno == ENOENT) ... /* RIGHT */
Prevent it by always using the E* constants from <errno.h>.
Mistake 4 — treating EOF or a short read as an error.
if (read(fd, buf, n) != n) handle_error(); /* WRONG */
read legitimately returns fewer bytes than requested, and 0 means EOF. Only -1 is an error. Check < 0 for errors and loop for partial reads.
Mistake 5 — setting errno on the success path of your own function. Leave errno untouched when you succeed; only set it when you return your sentinel.
Compiler errors.
'errno' undeclared — you forgot #include <errno.h>.implicit declaration of function 'strerror' — add #include <string.h>; for perror/fprintf add #include <stdio.h>.ssize_t and size_t when checking read results — cast deliberately, e.g. (size_t)n, only after you have confirmed n >= 0.Runtime symptoms.
Success even though the call failed: you read errno too late or never checked the return value. Snapshot errno immediately and verify you tested the sentinel.perror/fprintf.read: an EINTR retry loop with no other exit condition combined with a never-returning call — confirm the loop also exits on real errors and EOF.Logic errors. If error handling never triggers, print the return value too: fprintf(stderr, "rc=%d errno=%d (%s)\n", rc, errno, strerror(errno)); during debugging.
Questions to ask when it doesn't work.
errno?errno?E* name, not a number?EINTR (needs retry) rather than a fatal error?errno?This is not a security lesson, but errno handling intersects with several robustness and undefined-behaviour concerns:
open returns -1 and you ignore it, later read(-1, buf, n) simply fails again — but if you instead use an uninitialized fd (because you skipped the check), you may read/write a random descriptor. Always check before use.read returns the byte count; treat the buffer as containing exactly that many valid bytes. Never assume the buffer is full or null-terminated. Writing past n bytes is a buffer over-read/over-write.read/write return ssize_t (can be negative); storing the result in an unsigned type hides the -1 error as a huge positive number, defeating your check. Use ssize_t.strerror is not thread-safe. In multithreaded code, two threads calling strerror can corrupt each other's message. Use strerror_r with a caller-owned buffer to avoid a data race.perror/fprintf.errno lifetime. The snapshot you save is a plain int local — safe. The pointer returned by strerror may point into a shared/static buffer with limited lifetime; copy the text if you must keep it.Concrete uses.
cat, cp, and ls print exactly the errno-derived messages you see (cat: missing.txt: No such file or directory). That message is literally perror/strerror output with the program name as prefix.errno to distinguish a missing file (404), a permission problem (403), and resource exhaustion (EMFILE → 503 or backpressure). Network code retries on EINTR/EAGAIN.close/fsync errors via errno to know whether data actually reached disk before acknowledging a commit.errno so user programs get consistent diagnostics.Professional best-practice habits.
Beginner rules:
errno immediately into a local when you'll use it later.perror/strerror instead of inventing your own message text.<errno.h> and compare against E* constants.Advanced rules:
strerror_r in multithreaded programs.EINTR, EAGAIN) from fatal ones and handle each appropriately.errno: save it on entry and restore it on the success path if your function makes internal calls.strerror output are recorded consistently, without leaking sensitive data.Beginner 1 — Report a missing file. Write a program that takes a path as argv[1], calls open(path, O_RDONLY), and if it fails prints open <path>: <reason> to stderr using strerror(errno), then exits non-zero. Requirements: check the return value before reading errno; include the path in the message. Example: ./a.out nope → open nope: No such file or directory. Concepts: sentinel check, strerror.
Beginner 2 — perror practice. Modify the program above to use perror("open") instead of building the string yourself. Compare the two outputs. Hint: perror automatically appends ": " and the current message and writes to stderr. Concepts: perror, errno is read at call time.
Intermediate 1 — Branch on the cause. Extend the program to print a custom message for ENOENT ("file not found"), a different one for EACCES ("permission denied"), and fall back to strerror for any other code. Requirements: snapshot errno before the branching; use the symbolic constants. Constraint: no raw numeric comparisons. Concepts: specific error codes, errno snapshot.
Intermediate 2 — Save errno across a cleanup call. Write a function that opens a file, then on a simulated failure path calls close(fd) and afterward reports the original error correctly. Demonstrate that without saving errno the message is wrong, and with saving it is right. Hint: deliberately make close run between the failure and the report. Concepts: errno fragility, snapshotting.
Challenge — Your own errno-setting function. Implement int safe_read_all(int fd, void *buf, size_t len, size_t *out_n) that reads exactly len bytes (looping over partial reads, retrying on EINTR). On success return 0 and set *out_n. On EOF before len bytes, set errno = EIO (or your chosen code) and return -1. On a real read error, leave errno as read set it and return -1. Requirements: never set errno on the success path; handle partial reads; document which codes a caller may see. Constraints: no global state, no fixed-size assumptions. Concepts: the full failure protocol, EINTR retry, setting errno in your own API.
-1 or NULL) and explain it with errno. The return value answers "did it fail?"; errno answers "why?".errno immediately. Never infer failure from errno alone — it is not cleared on success.errno is thread-local but fragile: the next library call (even a successful one) can overwrite it. Snapshot it into a local variable if you'll use it after any other call.perror("context") or strerror(errno) (use strerror_r in threads). Compare against symbolic E* constants, never raw numbers.ENOENT, EACCES, EINTR, ...) to react correctly — retry on EINTR, report on EACCES, create on ENOENT.errno and return a sentinel on error; leave errno untouched on success.errno without checking the return value, letting another call clobber errno before you read it, comparing errno to a number, and treating EOF/short reads as errors.