Tutorials Logic, IN info@tutorialslogic.com

C Error Handling errno, perror, strerror: Causes and Fixes

C Failure Contracts

C APIs report failure through return values, sentinel values, output parameters, errno, or a combination. The caller must know which mechanism applies and check it before using output. errno is meaningful only when the called function documents setting it and the return value indicates failure.

C Error Signals

Unlike C++ or Java, C has no built-in exception mechanism. Functions report failure through return values, status flags, and the errno convention:

  • Return values - functions return a special value (e.g., -1, NULL, 0) to signal failure.
  • errno - a global integer variable set by system calls and library functions when an error occurs.
  • perror() - prints a human-readable error message to stderr.
  • strerror() - returns the error message string for a given errno value.

Return Value Pattern

The most fundamental C error handling pattern: check the return value of every function that can fail.

Checking Return Values

Checking Return Values
#include <stdio.h>
#include <stdlib.h>

// Function returns -1 on error, result on success
int divide(int a, int b, int *result) {
    if (b == 0) {
        return -1;  // error code
    }
    *result = a / b;
    return 0;  // success
}

int main() {
    int result;

    // Success case
    if (divide(10, 2, &result) == 0) {
        printf("10 / 2 = %d\n", result);
    } else {
        printf("Error: division failed\n");
    }

    // Error case
    if (divide(10, 0, &result) == 0) {
        printf("10 / 0 = %d\n", result);
    } else {
        printf("Error: cannot divide by zero\n");
    }

    // malloc returns NULL on failure
    int *arr = malloc(1000000000ULL * sizeof *arr);  // deliberately large request
    if (arr == NULL) {
        fprintf(stderr, "Error: memory allocation failed\n");
        return EXIT_FAILURE;
    }
    free(arr);

    return EXIT_SUCCESS;
}

errno, perror() and strerror()

errno is set by system calls when they fail. Always check errno immediately after a failed call - the next function call may overwrite it.

errno Code Value Meaning
ENOENT 2 No such file or directory
EACCES 13 Permission denied
ENOMEM 12 Out of memory
EINVAL 22 Invalid argument
ERANGE 34 Result out of range

errno, perror() and strerror() - C Example

errno, perror() and strerror() - C Example
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main() {
    // Try to open a file that doesn't exist
    FILE *fp = fopen("nonexistent.txt", "r");

    if (fp == NULL) {
        // errno is set by fopen on failure
        printf("errno value: %d\n", errno);

        // perror: prints "prefix: error message" to stderr
        perror("fopen failed");

        // strerror: returns the error string
        printf("Error: %s\n", strerror(errno));
    }

    // Reset errno before next call
    errno = 0;

    // Math error: log of negative number
    #include <math.h>
    double result = sqrt(-1.0);
    if (errno == EDOM) {
        perror("sqrt(-1)");  // sqrt(-1): Numerical argument out of domain
    }

    return 0;
}

/*
Output:
errno value: 2
fopen failed: No such file or directory
Error: No such file or directory
*/

Custom Error Handling Pattern

A clean pattern for larger programs: define your own error codes and a centralized error handler.

Custom Error Codes Pattern

Custom Error Codes Pattern
#include <stdio.h>
#include <stdlib.h>

// Custom error codes
typedef enum {
    ERR_OK       = 0,
    ERR_NULL_PTR = 1,
    ERR_DIV_ZERO = 2,
    ERR_OVERFLOW = 3,
    ERR_IO       = 4
} ErrorCode;

// Error message lookup
const char* errorMessage(ErrorCode code) {
    switch (code) {
        case ERR_OK:       return "Success";
        case ERR_NULL_PTR: return "Null pointer";
        case ERR_DIV_ZERO: return "Division by zero";
        case ERR_OVERFLOW: return "Integer overflow";
        case ERR_IO:       return "I/O error";
        default:           return "Unknown error";
    }
}

// Function using custom error codes
ErrorCode safeDivide(int a, int b, int *out) {
    if (out == NULL) return ERR_NULL_PTR;
    if (b == 0)      return ERR_DIV_ZERO;
    *out = a / b;
    return ERR_OK;
}

int main() {
    int result;
    ErrorCode err;

    err = safeDivide(10, 2, &result);
    if (err == ERR_OK) printf("10/2 = %d\n", result);
    else printf("Error: %s\n", errorMessage(err));

    err = safeDivide(10, 0, &result);
    if (err == ERR_OK) printf("10/0 = %d\n", result);
    else printf("Error: %s\n", errorMessage(err));  // Error: Division by zero

    err = safeDivide(10, 2, NULL);
    if (err == ERR_OK) printf("result = %d\n", result);
    else printf("Error: %s\n", errorMessage(err));  // Error: Null pointer

    return 0;
}

Cleanup on Failure

Acquire resources in a clear order and release acquired resources in reverse order. A single cleanup label can be appropriate in C when several failure paths own different subsets; initialize handles to safe values so cleanup is idempotent and never releases an unowned resource.

Preserve the original error before cleanup if later functions can change errno. Return stable application status codes rather than exposing platform messages as the only contract. Log enough context to diagnose the failed operation without printing credentials or sensitive input.

Before you move on

C Error Handling errno, perror, strerror: Causes and Fixes Mastery Check

4 checks
  • Unlike C++ or Java, C has no built-in exception mechanism.
  • I can choose between a return value, errno, and a caller-provided status for a C API.
  • errno is set by system calls when they fail.
  • Always check errno immediately after a failed call - the next function call may overwrite it.
Next Step
Next Practice

Finish the concept here, then reinforce it with hands-on coding, interview prep, or a tool that matches the topic.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.