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 read errno when that signals failure
  • Save errno to a local variable immediately — any subsequent call can overwrite it
  • perror prints to stderr with a fixed format; strerror returns the string for you to format
  • errno is not reset on success — stale values from previous calls are common
  • For strtol/strtod, reset errno = 0 before calling and check ERANGE after
  • Use named constants like ENOENT, EINVAL — not numeric values
  • Return errors early — do not defer error handling to the caller’s caller