C has no exceptions. When a system call or library function fails, it signals the error through two channels: a special return value (usually -1 or NULL), and a global variable called errno. Most C programmers check the return value but misuse or ignore errno. Here is the full picture.
What errno Is
#include <errno.h>
#include <stdio.h>
#include <string.h>
int main() {
FILE *fp = fopen("nonexistent.txt", "r");
if (fp == NULL) {
printf("errno value: %dn", errno);
printf("Error: %sn", strerror(errno));
}
return 0;
}
errno is set by system calls and library functions when they fail. After a failed fopen, it contains a code like ENOENT (2 on Linux — no such file or directory). strerror converts that code to a human-readable string.
On modern systems errno is actually a macro that expands to a per-thread variable — so it is thread-safe.
perror vs strerror
#include <errno.h>
#include <stdio.h>
#include <string.h>
int main() {
FILE *fp = fopen("/root/secret.txt", "r");
if (fp == NULL) {
/* perror: prints prefix + ": " + error string + newline */
perror("fopen failed");
/* strerror: returns the error string, you format it yourself */
fprintf(stderr, "Error opening file: %sn", strerror(errno));
}
return 0;
}
Output (assuming permission denied):
fopen failed: Permission denied
Error opening file: Permission denied
perror is convenient but prints to stderr with a fixed format. Use strerror when you need the error string embedded in a larger message or written to a log file.
The Critical Rule: Save errno Immediately
#include <errno.h>
#include <stdio.h>
#include <string.h>
int main() {
FILE *fp = fopen("missing.txt", "r");
if (fp == NULL) {
int saved_errno = errno; /* save BEFORE any other call */
printf("open failedn"); /* printf may reset errno! */
fprintf(stderr, "Error: %sn", strerror(saved_errno));
}
return 0;
}
Almost any system call or library function can overwrite errno. Even printf can change it if it calls underlying I/O functions. Save errno to a local variable immediately after the failed call, before doing anything else.
Common errno Codes
| Code | Value | Meaning |
|---|---|---|
ENOENT |
2 | No such file or directory |
EACCES |
13 | Permission denied |
ENOMEM |
12 | Out of memory (malloc failed) |
EINVAL |
22 | Invalid argument |
EEXIST |
17 | File already exists |
ENOBUFS |
105 | No buffer space available |
ETIMEDOUT |
110 | Connection timed out |
Always use the named constants, not the numbers — they differ between platforms.
Writing Functions That Return Errors
#include <errno.h>
#include <stdio.h>
#include <string.h>
/* Pattern 1: return -1 on failure, set errno */
int read_config(const char *path) {
FILE *fp = fopen(path, "r");
if (fp == NULL) {
return -1; /* errno is already set by fopen */
}
/* ... read file ... */
fclose(fp);
return 0;
}
/* Pattern 2: return error code directly */
int open_log(const char *path, FILE **out) {
FILE *fp = fopen(path, "a");
if (fp == NULL) {
return errno; /* caller gets the code */
}
*out = fp;
return 0;
}
int main() {
if (read_config("config.txt") == -1) {
perror("read_config");
return 1;
}
FILE *log;
int err = open_log("/var/log/app.log", &log);
if (err != 0) {
fprintf(stderr, "open_log: %sn", strerror(err));
return 1;
}
return 0;
}
Both patterns are common in C codebases. Pattern 1 (return -1, set errno) matches POSIX conventions. Pattern 2 (return the error code) is more self-documenting. Pick one and stick to it within a module.
errno Is Not Reset on Success
#include <errno.h>
#include <stdio.h>
int main() {
errno = 0; /* reset manually before checking */
int result = some_function_that_might_fail();
/* Some functions return a valid value AND set errno on success */
/* Only check errno if the return value indicates failure */
if (result == -1) {
perror("some_function");
}
return 0;
}
errno is not cleared between calls. If a previous call set errno = EINVAL and a subsequent call succeeds without touching errno, you will see stale garbage in errno. Always check the return value first — only look at errno when the return value signals failure.
The strtol / strtod Special Case
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
int main() {
const char *str = "99999999999999999999"; /* overflow */
char *endptr;
errno = 0; /* MUST reset before calling strtol */
long result = strtol(str, &endptr, 10);
if (errno == ERANGE) {
fprintf(stderr, "Overflow: %sn", str);
return 1;
}
if (endptr == str) {
fprintf(stderr, "Not a numbern");
return 1;
}
printf("Result: %ldn", result);
return 0;
}
strtol and strtod are among the few functions where you MUST reset errno to 0 before calling, because they use ERANGE to signal overflow even when they return a meaningful value (LONG_MAX or LONG_MIN). If you forget to clear errno first, you may falsely detect overflow from a previous call. You can try this yourself in our run c online compiler.
TL;DR
- Check the return value first (
-1,NULL, etc.) — only readerrnowhen that signals failure - Save
errnoto a local variable immediately — any subsequent call can overwrite it perrorprints to stderr with a fixed format;strerrorreturns the string for you to formaterrnois not reset on success — stale values from previous calls are common- For
strtol/strtod, reseterrno = 0before calling and checkERANGEafter - Use named constants like
ENOENT,EINVAL— not numeric values - Return errors early — do not defer error handling to the caller’s caller