C Error Handling — errno, perror, strerror, and Robust Patterns
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
Not checking return values: A common source of bugs is assuming fopen, malloc, fgets, fwrite, etc. always succeed. Every system call can fail.
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.
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.
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.
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
- When is errno set by a function, and when is it undefined?
- Why is the goto cleanup pattern preferred over nested if-else in C?
- What is the difference between perror and strerror?
- Why must signal handlers only call async-signal-safe functions?
- 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
What is Next
Proceed to Signals to learn about asynchronous signal handling with signal() and sigaction(). Then continue with Multithreading for concurrent programming.