Skip to content

C Error Handling — errno, perror, strerror, and Robust Patterns

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about C Error Handling. We cover key concepts, practical examples, and best practices to help you master this topic.

C error handling uses return values and errno: system calls set errno on failure, perror prints descriptive messages, strerror converts error codes to strings, and consistent patterns with goto cleanup ensure resources are freed on all code paths.

What You Will Learn

  • The errno variable and its purpose
  • Using perror and strerror for error messages
  • Checking return values of standard functions
  • The goto cleanup pattern for resource management
  • Creating custom error codes and error reporting
  • Assertions for invariant checking
  • Signal-safe error handling

Why It Matters

C has no exceptions or try-catch. Every function call must be checked for errors, and resources (memory, file handles, sockets) must be released on every code path. A program that ignores errors crashes at best and corrupts data at worst. Durga Antivirus Pro must handle disk-full errors during quarantine, network timeouts during update downloads, and permission-denied errors during file scanning without leaking resources.

Real-World Use

A database server processes 10,000 queries per second. Each query allocates memory, opens a cursor, reads from disk, and sends results over the network. If any step fails, all previously allocated resources for that query must be freed. The cleanup pattern ensures this happens consistently.

Learning Path

flowchart LR
  A[String Handling] --> B[Error Handling\nYou are here]
  B --> C[Signals]
  style B fill:#f90,color:#fff

Basic errno Usage

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main() {
    FILE *file = fopen("/nonexistent/file.txt", "r");
    if (file == NULL) {
        printf("errno value: %d\n", errno);
        printf("strerror: %s\n", strerror(errno));
        perror("fopen");  // Prints: fopen: No such file or directory
        return 1;
    }
    fclose(file);
    return 0;
}

Checking Every Function Call

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

int main() {
    size_t size = 1024 * 1024;  // 1 MB
    int *data = malloc(size * sizeof(int));
    if (data == NULL) {
        perror("malloc failed");
        return 1;
    }

    FILE *file = fopen("output.dat", "wb");
    if (file == NULL) {
        perror("fopen failed");
        free(data);
        return 1;
    }

    size_t written = fwrite(data, sizeof(int), size, file);
    if (written != size) {
        if (ferror(file)) {
            perror("fwrite failed");
        } else {
            fprintf(stderr, "Short write: %zu of %zu\n", written, size);
        }
        fclose(file);
        free(data);
        return 1;
    }

    if (fclose(file) != 0) {
        perror("fclose failed");
    }

    free(data);
    printf("Successfully wrote %zu ints\n", size);
    return 0;
}

Goto Cleanup Pattern

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

#define CHECK(cond, label) do { \
    if (!(cond)) { \
        perror(#cond); \
        goto label; \
    } \
} while (0)

int process_file(const char *input, const char *output) {
    FILE *fin = NULL;
    FILE *fout = NULL;
    char *buffer = NULL;
    int ret = -1;

    fin = fopen(input, "rb");
    CHECK(fin != NULL, cleanup);

    fout = fopen(output, "wb");
    CHECK(fout != NULL, cleanup);

    buffer = malloc(65536);
    CHECK(buffer != NULL, cleanup);

    size_t n;
    while ((n = fread(buffer, 1, 65536, fin)) > 0) {
        if (fwrite(buffer, 1, n, fout) != n) {
            CHECK(0, cleanup);  // fwrite failed
        }
    }
    CHECK(!ferror(fin), cleanup);

    ret = 0;

cleanup:
    if (ret != 0) {
        fprintf(stderr, "Failed to process file\n");
    }
    free(buffer);
    if (fin) fclose(fin);
    if (fout) fclose(fout);
    return ret;
}

int main() {
    if (process_file("input.dat", "output.dat") != 0) {
        return 1;
    }
    printf("File processed successfully\n");
    return 0;
}

Custom Error Codes

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

typedef enum {
    ERR_OK = 0,
    ERR_NOMEM = -1,
    ERR_NOT_FOUND = -2,
    ERR_INVALID_INPUT = -3,
    ERR_IO_ERROR = -4,
    ERR_NETWORK = -5
} ErrorCode;

const char* error_string(ErrorCode err) {
    switch (err) {
        case ERR_OK: return "Success";
        case ERR_NOMEM: return "Out of memory";
        case ERR_NOT_FOUND: return "Not found";
        case ERR_INVALID_INPUT: return "Invalid input";
        case ERR_IO_ERROR: return "I/O error";
        case ERR_NETWORK: return "Network error";
        default: return "Unknown error";
    }
}

typedef struct {
    int *data;
    size_t size;
    size_t capacity;
} Vector;

ErrorCode vector_init(Vector *v, size_t initial_capacity) {
    v->data = malloc(initial_capacity * sizeof(int));
    if (!v->data) return ERR_NOMEM;
    v->size = 0;
    v->capacity = initial_capacity;
    return ERR_OK;
}

ErrorCode vector_push(Vector *v, int value) {
    if (v->size >= v->capacity) {
        size_t new_cap = v->capacity * 2;
        int *new_data = realloc(v->data, new_cap * sizeof(int));
        if (!new_data) return ERR_NOMEM;
        v->data = new_data;
        v->capacity = new_cap;
    }
    v->data[v->size++] = value;
    return ERR_OK;
}

void vector_free(Vector *v) {
    free(v->data);
    v->data = NULL;
    v->size = 0;
    v->capacity = 0;
}

int main() {
    Vector v;
    ErrorCode err;

    err = vector_init(&v, 4);
    if (err != ERR_OK) {
        fprintf(stderr, "Init failed: %s\n", error_string(err));
        return 1;
    }

    for (int i = 0; i < 100; i++) {
        err = vector_push(&v, i * i);
        if (err != ERR_OK) {
            fprintf(stderr, "Push failed at %d: %s\n", i, error_string(err));
            vector_free(&v);
            return 1;
        }
    }

    printf("Vector has %zu elements\n", v.size);
    vector_free(&v);
    return 0;
}

Assertions for Invariants

#include <stdio.h>
#include <assert.h>
#include <string.h>

// Enable assertions (default in debug builds)
// #define NDEBUG would disable all assertions

typedef struct {
    char data[256];
    size_t length;
} SafeString;

void ss_init(SafeString *s) {
    s->data[0] = '\0';
    s->length = 0;
}

void ss_append(SafeString *s, const char *text) {
    size_t text_len = strlen(text);
    assert(s->length + text_len < sizeof(s->data));
    strcpy(s->data + s->length, text);
    s->length += text_len;
}

char ss_get(const SafeString *s, size_t index) {
    assert(index < s->length);
    return s->data[index];
}

int main() {
    SafeString s;
    ss_init(&s);
    ss_append(&s, "Hello, ");
    ss_append(&s, "world!");

    for (size_t i = 0; i < s.length; i++) {
        putchar(ss_get(&s, i));
    }
    putchar('\n');

    // This triggers assertion failure in debug builds
    // char c = ss_get(&s, 1000);

    return 0;
}

Signal-Safe Error Handling

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>

// Signal handlers can only use async-signal-safe functions
volatile sig_atomic_t error_flag = 0;
int saved_errno = 0;

void signal_handler(int sig) {
    saved_errno = errno;
    error_flag = 1;

    const char msg[] = "Signal caught!\n";
    write(STDERR_FILENO, msg, sizeof(msg) - 1);
}

void do_risky_operation(void) {
    // Simulate an operation that might fail
    FILE *f = fopen("/dev/null", "r");
    if (!f) {
        saved_errno = errno;
        error_flag = 1;
    }
}

int main() {
    struct sigaction sa;
    sa.sa_handler = signal_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGINT, &sa, NULL);

    printf("Press Ctrl+C to test signal handling\n");

    while (!error_flag) {
        printf("Working...\n");
        sleep(1);
    }

    if (error_flag) {
        fprintf(stderr, "Error detected: %s\n", strerror(saved_errno));
    }

    return 0;
}

Common Mistakes

  1. Not checking return values: A common source of bugs is assuming fopen, malloc, fgets, fwrite, etc. always succeed. Every system call can fail.

  2. Using errno without checking the return value first: errno is only meaningful if the preceding call actually failed. A successful call does not reset errno to 0. Always check the return value first.

  3. Resetting errno to 0 before system calls: Some functions set errno on success (e.g., fprintf may set errno for some implementations). Always set errno = 0 before the call if you need to detect errors.

  4. Resource leaks on error paths: If malloc succeeds but fopen fails, the code must free the malloc'd memory before returning. Use the goto cleanup pattern consistently.

  5. Calling non-signal-safe functions in signal handlers: Functions like printf, malloc, and free are not signal-safe. Use only write() and a few others in signal handlers. Set a volatile sig_atomic_t flag and handle the error in the main loop.

Practice Questions

  1. When is errno set by a function, and when is it undefined?
  2. Why is the goto cleanup pattern preferred over nested if-else in C?
  3. What is the difference between perror and strerror?
  4. Why must signal handlers only call async-signal-safe functions?
  5. Challenge: Write a robust file copy program that handles all error conditions: input file does not exist, output file cannot be created, disk full during write, memory allocation failure for the buffer, partial read with error, and signal interruption (EINTR). Use the goto cleanup pattern. Print specific error messages for each case.

Mini Project

Build a robust configuration file loader:

  • Reads key=value pairs from a text file
  • Handles errors: file not found, permission denied, malformed line, duplicate key, memory allocation failure, value too long
  • Returns a struct with an ErrorCode and an ErrorMessage string
  • Uses goto cleanup for all resource allocation
  • Provides a function config_error_string(ErrorCode) that returns a human-readable message
  • Allocates memory for the config struct and all key/value strings
  • Includes a config_free function that frees everything
  • Tests: missing file, empty file, valid file, file with syntax errors, extremely long values

FAQ

Should I set errno to 0 before every call?

For standard library calls, no. But before library calls that may set errno on error, setting errno = 0 before the call and checking after is a good practice, especially for functions like strtol.

What is the difference between perror and fprintf(stderr, ...)?

perror appends the system error message for the current errno value. fprintf requires you to call strerror(errno) manually. perror is simpler; fprintf gives more control over the message format.

Can I throw an exception in C?

C has no exceptions. You simulate them with setjmp/longjmp (for non-local jumps) or by returning error codes. Most C code uses return codes for error handling.

What is EINTR and how should I handle it?

EINTR means a system call was interrupted by a signal. The call did not fail; it was just interrupted. You should retry the call: while ((n = read(fd, buf, len)) < 0 && errno == EINTR) continue;

How do I handle errors in multithreaded code?

Each thread has its own errno. Use thread-safe error reporting (return codes, per-thread error structures). Avoid modifying shared error state without synchronization.

What is Next

Proceed to Signals to learn about asynchronous signal handling with signal() and sigaction(). Then continue with Multithreading for concurrent programming.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro

Home Browse C