C in 100 Seconds: Error Handling
Video: C in 100 Seconds: Error Handling — errno, perror, strerror | Episode 39 by Taught by Celeste AI - AI Coding Coach
C Error Handling: errno, perror, strerror
System calls return
-1orNULLon failure and seterrno.perror("context")printscontext: error message.strerror(errno)returns the string. The standard pattern for any C system code.
C doesn't have exceptions. System functions signal errors via return values plus the global errno. Knowing how to read these is half of writing reliable C.
The basic shape
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
int main() {
FILE *f = fopen("/tmp/no_such_file.txt", "r");
if (f == NULL) {
perror("fopen failed");
// Output: "fopen failed: No such file or directory"
}
errno = 0;
FILE *f2 = fopen("/etc/master.passwd", "r");
if (f2 == NULL) {
printf("errno: %d\n", errno);
printf("error: %s\n", strerror(errno));
}
int a = 10, b = 0;
if (b == 0) {
fprintf(stderr, "Error: division by zero\n");
} else {
printf("%d / %d = %d\n", a, b, a / b);
}
return 0;
}
errno
errno is a global integer set by failing system calls. Defined in <errno.h>.
#include <errno.h>
errno = 0; // reset before the call
fopen("missing", "r");
if (errno != 0) {
printf("Error code: %d\n", errno);
}
Common error codes (from <errno.h>):
ENOENT— No such file or directory.EACCES— Permission denied.EEXIST— File exists.ENOMEM— Out of memory.EINVAL— Invalid argument.EAGAIN— Try again (resource temporarily unavailable).
Numeric values vary by system; always use the name.
perror — print with context
FILE *f = fopen("missing", "r");
if (f == NULL) {
perror("fopen failed");
// Output: "fopen failed: No such file or directory"
}
perror(prefix) prints prefix: <human-readable error> to stderr. The prefix is your context; the message comes from errno.
For "the program failed because perror is the one-liner.
strerror
printf("Error: %s\n", strerror(errno));
strerror(n) returns a pointer to a human-readable string for error code n. Same message as perror uses, but as a string you can format yourself.
The returned pointer points to a static buffer — calling strerror again may overwrite. For thread-safe use, strerror_r (POSIX).
When errno is set
Functions that can fail and set errno:
- File:
fopen,read,write,open,close. - Memory:
malloc(setsENOMEMon failure). - Network:
socket,connect,recv,send. - Process:
fork,exec.
Functions that don't set errno:
- Many string functions (
strlen,strcmp— these can't fail in interesting ways). - Pure-computation functions.
The convention: check the return value first; consult errno only if it indicated failure.
Reset before checking
errno = 0;
long n = strtol(input, NULL, 10);
if (errno != 0) {
perror("strtol");
}
Some functions (strtol, math functions) set errno on overflow/error but don't return a sentinel. Reset to 0 before the call so you can detect a fresh error.
For most system calls that return -1 or NULL on failure, you don't need to reset — the error is signaled by the return.
stderr — the error stream
fprintf(stderr, "Error: invalid input\n");
stderr is the standard error stream. By convention, errors and diagnostics go here; normal output goes to stdout. Users can redirect them separately:
./prog 2>errors.log >output.txt
2> redirects stderr; > redirects stdout.
For a program that produces machine-readable output, this lets users filter signal from noise.
Return codes — exit status
int main() {
if (something_failed) {
fprintf(stderr, "Failed\n");
return 1;
}
return 0;
}
main's return value is the program's exit status. 0 = success; non-zero = failure. The shell can check it:
./prog
echo $? # 0 or whatever
Conventional codes:
0— success.1— generic failure.2— usage error (bad command line).64-78— sysexits.h codes (POSIX).
exit(code) (from <stdlib.h>) terminates the process with the given code.
Defensive programming pattern
int read_data(const char *path) {
FILE *f = fopen(path, "r");
if (f == NULL) {
fprintf(stderr, "%s: %s\n", path, strerror(errno));
return -1;
}
char *buf = malloc(1024);
if (buf == NULL) {
fclose(f);
fprintf(stderr, "out of memory\n");
return -1;
}
size_t n = fread(buf, 1, 1024, f);
if (ferror(f)) {
perror("fread");
free(buf);
fclose(f);
return -1;
}
// ... use buf ...
free(buf);
fclose(f);
return 0;
}
Every system call gets checked. Resources are freed on every error path. The pattern is verbose; goto-cleanup helps:
int read_data(const char *path) {
FILE *f = NULL;
char *buf = NULL;
int rc = -1;
if ((f = fopen(path, "r")) == NULL) goto cleanup;
if ((buf = malloc(1024)) == NULL) goto cleanup;
size_t n = fread(buf, 1, 1024, f);
if (ferror(f)) goto cleanup;
rc = 0;
cleanup:
free(buf); // free(NULL) is safe
if (f) fclose(f);
return rc;
}
Checking specific errors
if (mkdir("/path", 0755) == -1) {
if (errno == EEXIST) {
// already exists — fine
} else {
perror("mkdir");
return -1;
}
}
Sometimes "the file already exists" is not really an error. Check errno to decide.
Common mistakes
Not checking returns. "The fopen worked, right?" — until it doesn't. Always check.
Using errno without checking the return first. Some functions set errno on success too (cleared between calls is implementation-defined). Always: check return → if failed → check errno.
strerror not thread-safe. Shared static buffer. Use strerror_r in threaded code.
Logging to stdout instead of stderr. Mixes errors with data, breaks pipes.
Generic exit codes. return 1 everywhere is fine for small programs; for complex tools, distinguish error categories.
Not freeing on error paths. Leaks. Use goto-cleanup or wrappers.
What's next
Episode 40: preprocessor directives — #define, #ifdef, #ifndef. The text-substitution layer underneath C.
Recap
System functions signal errors via return value + errno. perror("context") prints to stderr with a message from errno. strerror(errno) returns the string for formatted output. Always check return values; consult errno only when the call actually failed. Reset errno to 0 before functions that don't return a sentinel (strtol). Output errors to stderr, data to stdout. Return non-zero exit code on failure. Use goto-cleanup for unified resource management.
Next episode: preprocessor directives.