Part of C in 100s

C in 100 Seconds: Error Handling

Celest KimCelest Kim

Video: C in 100 Seconds: Error Handling — errno, perror, strerror | Episode 39 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

C Error Handling: errno, perror, strerror

System calls return -1 or NULL on failure and set errno. perror("context") prints context: 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 said ," 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 (sets ENOMEM on 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.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.