Building Resilient Systems: A Complete Guide to Error Handling with errno in C

Error handling is the bedrock of robust C programming. Unlike modern languages with exception mechanisms, C relies on a simple but powerful system centered around errno. Mastering errno is essential for writing reliable, production-grade C code that gracefully handles failures. This comprehensive guide explores every facet of errno-based error handling, from basic usage to advanced patterns.

What is errno?

errno (error number) is a global integer variable defined in <errno.h> that system calls and library functions set to indicate what went wrong. When a function fails, it typically returns a sentinel value (like -1 or NULL) and sets errno to a specific error code.

#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
extern int errno;  // Actually defined in errno.h

The errno Landscape

1. Common errno Values

#include <errno.h>
#include <stdio.h>
void print_common_errno_values() {
printf("Common errno values:\n");
printf("  EPERM   : %d - Operation not permitted\n", EPERM);
printf("  ENOENT  : %d - No such file or directory\n", ENOENT);
printf("  ESRCH   : %d - No such process\n", ESRCH);
printf("  EINTR   : %d - Interrupted system call\n", EINTR);
printf("  EIO     : %d - I/O error\n", EIO);
printf("  ENXIO   : %d - No such device or address\n", ENXIO);
printf("  E2BIG   : %d - Argument list too long\n", E2BIG);
printf("  ENOEXEC : %d - Exec format error\n", ENOEXEC);
printf("  EBADF   : %d - Bad file number\n", EBADF);
printf("  ECHILD  : %d - No child processes\n", ECHILD);
printf("  EAGAIN  : %d - Try again\n", EAGAIN);
printf("  ENOMEM  : %d - Out of memory\n", ENOMEM);
printf("  EACCES  : %d - Permission denied\n", EACCES);
printf("  EFAULT  : %d - Bad address\n", EFAULT);
printf("  EBUSY   : %d - Device or resource busy\n", EBUSY);
printf("  EEXIST  : %d - File exists\n", EEXIST);
printf("  EXDEV   : %d - Cross-device link\n", EXDEV);
printf("  ENODEV  : %d - No such device\n", ENODEV);
printf("  ENOTDIR : %d - Not a directory\n", ENOTDIR);
printf("  EISDIR  : %d - Is a directory\n", EISDIR);
printf("  EINVAL  : %d - Invalid argument\n", EINVAL);
printf("  ENFILE  : %d - File table overflow\n", ENFILE);
printf("  EMFILE  : %d - Too many open files\n", EMFILE);
printf("  ENOTTY  : %d - Not a typewriter\n", ENOTTY);
printf("  EFBIG   : %d - File too large\n", EFBIG);
printf("  ENOSPC  : %d - No space left on device\n", ENOSPC);
printf("  ESPIPE  : %d - Illegal seek\n", ESPIPE);
printf("  EROFS   : %d - Read-only file system\n", EROFS);
printf("  EPIPE   : %d - Broken pipe\n", EPIPE);
printf("  EDOM    : %d - Math argument out of domain\n", EDOM);
printf("  ERANGE  : %d - Math result not representable\n", ERANGE);
}

2. errno Categories

// Error categories by domain
void categorize_errno(int err) {
printf("errno %d: ", err);
// File system errors
if (err == ENOENT || err == EACCES || err == EPERM || 
err == ENOTDIR || err == EISDIR || err == EROFS ||
err == ENOSPC || err == EFBIG || err == EMLINK) {
printf("File system error\n");
}
// Resource errors
else if (err == ENOMEM || err == ENFILE || err == EMFILE ||
err == ENOSPC || err == EBUSY) {
printf("Resource error\n");
}
// I/O errors
else if (err == EIO || err == EAGAIN || err == EWOULDBLOCK ||
err == EPIPE || err == ENXIO) {
printf("I/O error\n");
}
// Process errors
else if (err == ECHILD || err == ESRCH || err == EPERM) {
printf("Process error\n");
}
// Argument errors
else if (err == EINVAL || err == EFAULT || err == E2BIG) {
printf("Argument error\n");
}
// Math errors
else if (err == EDOM || err == ERANGE) {
printf("Math error\n");
}
// Network errors
else if (err == ECONNREFUSED || err == ETIMEDOUT || 
err == ENETUNREACH) {
printf("Network error\n");
}
else {
printf("Other error\n");
}
}

Basic errno Usage Patterns

1. The Essential Pattern

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
void basic_errno_pattern() {
FILE *file = fopen("nonexistent.txt", "r");
if (file == NULL) {
// Check errno immediately after failure
int saved_errno = errno;  // Save immediately
fprintf(stderr, "Error opening file: %s (errno=%d)\n", 
strerror(saved_errno), saved_errno);
// Handle specific errors
switch (saved_errno) {
case ENOENT:
fprintf(stderr, "  -> File doesn't exist. Creating default...\n");
// Create default file
break;
case EACCES:
fprintf(stderr, "  -> Permission denied. Check file permissions.\n");
break;
case ENOMEM:
fprintf(stderr, "  -> Out of memory. Cannot proceed.\n");
exit(1);
default:
fprintf(stderr, "  -> Unknown error occurred.\n");
}
} else {
fclose(file);
}
}

2. Clearing errno Before Critical Calls

void clear_errno_before_call() {
errno = 0;  // Clear before call
long result = strtol("123abc", NULL, 10);
if (errno != 0) {
// Error occurred during conversion
fprintf(stderr, "Conversion error: %s\n", strerror(errno));
} else {
printf("Conversion successful: %ld\n", result);
}
}

3. perror() for Quick Error Messages

#include <stdio.h>
#include <errno.h>
void perror_demo() {
FILE *file = fopen("/root/secret.txt", "r");
if (file == NULL) {
// Prints: "Cannot open file: Permission denied"
perror("Cannot open file");
// Equivalent to:
// fprintf(stderr, "Cannot open file: %s\n", strerror(errno));
}
}

Thread Safety and errno

1. Thread-Local Storage

#include <pthread.h>
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
void* thread_worker(void* arg) {
int thread_num = *(int*)arg;
// Each thread has its own errno
FILE *file = fopen("/nonexistent", "r");
if (file == NULL) {
printf("Thread %d: errno = %d (%s)\n", 
thread_num, errno, strerror(errno));
}
// Simulate work
sleep(1);
// Another operation that might set errno
int result = write(-1, "test", 4);  // Invalid file descriptor
if (result == -1) {
printf("Thread %d: errno = %d (%s)\n", 
thread_num, errno, strerror(errno));
}
return NULL;
}
void demonstrate_thread_safety() {
pthread_t threads[3];
int ids[3] = {1, 2, 3};
for (int i = 0; i < 3; i++) {
pthread_create(&threads[i], NULL, thread_worker, &ids[i]);
}
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
}

2. errno as a Macro (POSIX)

// On modern systems, errno is a macro that expands to a thread-local value
// __thread int errno;  // GCC thread-local storage
// #define errno (*__errno_location())  // Typical implementation
#include <stdio.h>
void demonstrate_errno_location() {
// Get pointer to thread-local errno
int *errno_ptr = __errno_location();
errno = ENOENT;
printf("errno = %d, *errno_ptr = %d\n", errno, *errno_ptr);
printf("Same location? %s\n", &errno == errno_ptr ? "Yes" : "No");
}

Advanced Error Handling Patterns

1. The Check and Save Pattern

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int errnum;
char message[256];
char function[64];
int line;
} ErrorInfo;
ErrorInfo last_error;
void save_error(int errnum, const char *func, int line, const char *msg) {
last_error.errnum = errnum;
last_error.line = line;
strncpy(last_error.function, func, sizeof(last_error.function) - 1);
snprintf(last_error.message, sizeof(last_error.message), "%s", msg);
}
#define TRY(expr) \
do { \
errno = 0; \
if ((expr) < 0) { \
save_error(errno, __func__, __LINE__, "Failed at " #expr); \
return -1; \
} \
} while(0)
#define TRY_PTR(expr) \
do { \
errno = 0; \
void *result = (expr); \
if (result == NULL) { \
save_error(errno, __func__, __LINE__, "Failed at " #expr); \
return NULL; \
} \
} while(0)
// Example usage
int copy_file_safe(const char *src, const char *dst) {
FILE *source, *dest;
char buffer[4096];
size_t bytes;
TRY_PTR(source = fopen(src, "rb"));
TRY_PTR(dest = fopen(dst, "wb"));
while ((bytes = fread(buffer, 1, sizeof(buffer), source)) > 0) {
if (fwrite(buffer, 1, bytes, dest) != bytes) {
save_error(errno, __func__, __LINE__, "Write failed");
fclose(source);
fclose(dest);
return -1;
}
}
fclose(source);
fclose(dest);
return 0;
}
void print_last_error() {
if (last_error.errnum != 0) {
fprintf(stderr, "Error [%s:%d]: %s\n", 
last_error.function, last_error.line, last_error.message);
fprintf(stderr, "  System error: %s (errno=%d)\n",
strerror(last_error.errnum), last_error.errnum);
}
}

2. Error Propagation with Cleanup

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
typedef enum {
ERR_SUCCESS = 0,
ERR_OPEN,
ERR_READ,
ERR_WRITE,
ERR_CLOSE,
ERR_MEMORY
} ErrorCode;
typedef struct {
ErrorCode code;
int sys_errno;
char file[64];
int line;
} DetailedError;
DetailedError g_error;
void clear_error() {
memset(&g_error, 0, sizeof(DetailedError));
}
void set_error(ErrorCode code, const char *file, int line) {
g_error.code = code;
g_error.sys_errno = errno;
strncpy(g_error.file, file, sizeof(g_error.file) - 1);
g_error.line = line;
}
#define SET_ERROR(code) set_error(code, __FILE__, __LINE__)
// Resource cleanup with error handling
int process_file(const char *filename) {
int fd = -1;
char *buffer = NULL;
ssize_t bytes_read;
int ret = -1;
clear_error();
// Open file
fd = open(filename, O_RDONLY);
if (fd == -1) {
SET_ERROR(ERR_OPEN);
goto cleanup;
}
// Allocate buffer
buffer = malloc(4096);
if (buffer == NULL) {
SET_ERROR(ERR_MEMORY);
goto cleanup;
}
// Read from file
bytes_read = read(fd, buffer, 4096);
if (bytes_read == -1) {
SET_ERROR(ERR_READ);
goto cleanup;
}
// Process data (simulate)
if (bytes_read == 0) {
// Empty file - not an error
ret = 0;
goto cleanup;
}
// Write to stdout
if (write(STDOUT_FILENO, buffer, bytes_read) != bytes_read) {
SET_ERROR(ERR_WRITE);
goto cleanup;
}
ret = 0;
cleanup:
// Always clean up resources
if (buffer) free(buffer);
if (fd != -1) close(fd);
return ret;
}
void print_detailed_error() {
if (g_error.code != ERR_SUCCESS) {
const char *error_names[] = {
"SUCCESS", "OPEN", "READ", "WRITE", "CLOSE", "MEMORY"
};
fprintf(stderr, "Error: %s at %s:%d\n", 
error_names[g_error.code], g_error.file, g_error.line);
if (g_error.sys_errno != 0) {
fprintf(stderr, "System error: %s (errno=%d)\n",
strerror(g_error.sys_errno), g_error.sys_errno);
}
}
}

3. Retry Logic for Transient Errors

#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#define MAX_RETRIES 5
#define RETRY_DELAY_US 100000  // 100ms
// Safe write with retry on EINTR and EAGAIN
ssize_t safe_write(int fd, const void *buf, size_t count) {
size_t total_written = 0;
const char *ptr = (const char*)buf;
int retries = 0;
while (total_written < count && retries < MAX_RETRIES) {
ssize_t written = write(fd, ptr + total_written, count - total_written);
if (written == -1) {
if (errno == EINTR) {
// Interrupted by signal, retry immediately
continue;
} else if (errno == EAGAIN || errno == EWOULDBLOCK) {
// Resource temporarily unavailable, wait and retry
retries++;
usleep(RETRY_DELAY_US);
continue;
} else {
// Real error
return -1;
}
}
total_written += written;
retries = 0;  // Reset retry count on successful write
}
return total_written;
}
// Safe read with retry
ssize_t safe_read(int fd, void *buf, size_t count) {
size_t total_read = 0;
char *ptr = (char*)buf;
int retries = 0;
while (total_read < count && retries < MAX_RETRIES) {
ssize_t bytes_read = read(fd, ptr + total_read, count - total_read);
if (bytes_read == -1) {
if (errno == EINTR) {
continue;
} else if (errno == EAGAIN || errno == EWOULDBLOCK) {
retries++;
usleep(RETRY_DELAY_US);
continue;
} else {
return -1;
}
} else if (bytes_read == 0) {
// EOF
break;
}
total_read += bytes_read;
retries = 0;
}
return total_read;
}

System Call Error Handling

1. File Operations

#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <errno.h>
int robust_file_copy(const char *src, const char *dst) {
int src_fd = -1, dst_fd = -1;
char buffer[8192];
ssize_t bytes;
int ret = -1;
// Open source file
src_fd = open(src, O_RDONLY);
if (src_fd == -1) {
fprintf(stderr, "Cannot open source '%s': %s\n", 
src, strerror(errno));
goto cleanup;
}
// Open destination file with proper permissions
dst_fd = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (dst_fd == -1) {
fprintf(stderr, "Cannot open destination '%s': %s\n", 
dst, strerror(errno));
goto cleanup;
}
// Copy data
while ((bytes = read(src_fd, buffer, sizeof(buffer))) > 0) {
if (safe_write(dst_fd, buffer, bytes) != bytes) {
fprintf(stderr, "Write error: %s\n", strerror(errno));
goto cleanup;
}
}
if (bytes == -1) {
fprintf(stderr, "Read error: %s\n", strerror(errno));
goto cleanup;
}
// Ensure data is written to disk
if (fsync(dst_fd) == -1) {
fprintf(stderr, "fsync error: %s\n", strerror(errno));
// Continue anyway
}
ret = 0;
cleanup:
if (src_fd != -1) close(src_fd);
if (dst_fd != -1) close(dst_fd);
return ret;
}

2. Network Operations

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <stdio.h>
int create_server_socket(int port) {
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1) {
perror("socket");
return -1;
}
// Set socket options to reuse address
int opt = 1;
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
perror("setsockopt");
close(sock);
return -1;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("bind");
close(sock);
return -1;
}
if (listen(sock, 10) == -1) {
perror("listen");
close(sock);
return -1;
}
return sock;
}
int accept_with_timeout(int sock, int timeout_sec) {
struct timeval tv;
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(sock, &readfds);
tv.tv_sec = timeout_sec;
tv.tv_usec = 0;
int ret = select(sock + 1, &readfds, NULL, NULL, &tv);
if (ret == -1) {
if (errno == EINTR) {
fprintf(stderr, "select interrupted by signal\n");
return -2;
}
perror("select");
return -1;
} else if (ret == 0) {
fprintf(stderr, "Timeout waiting for connection\n");
return -3;
}
int client_sock = accept(sock, NULL, NULL);
if (client_sock == -1) {
perror("accept");
return -1;
}
return client_sock;
}

3. Process Management

#include <sys/wait.h>
#include <signal.h>
#include <errno.h>
pid_t robust_fork() {
pid_t pid = fork();
if (pid == -1) {
if (errno == EAGAIN) {
fprintf(stderr, "System resource limit reached, cannot fork\n");
} else if (errno == ENOMEM) {
fprintf(stderr, "Out of memory, cannot fork\n");
} else {
perror("fork");
}
return -1;
}
return pid;
}
int robust_waitpid(pid_t pid, int *status, int options) {
int ret;
while ((ret = waitpid(pid, status, options)) == -1) {
if (errno == EINTR) {
// Interrupted by signal, retry
continue;
}
perror("waitpid");
return -1;
}
return ret;
}

Custom Error Handling Framework

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
// Error levels
typedef enum {
ERROR_LEVEL_DEBUG = 0,
ERROR_LEVEL_INFO,
ERROR_LEVEL_WARNING,
ERROR_LEVEL_ERROR,
ERROR_LEVEL_FATAL
} ErrorLevel;
// Error context
typedef struct {
ErrorLevel level;
int errnum;
char file[64];
char function[64];
int line;
char message[512];
struct ErrorContext *next;
} ErrorContext;
// Error handler function type
typedef void (*ErrorHandler)(const ErrorContext *ctx);
// Global error handling state
static ErrorHandler global_handler = NULL;
static ErrorContext *error_stack = NULL;
// Default error handler
void default_error_handler(const ErrorContext *ctx) {
const char *level_names[] = {"DEBUG", "INFO", "WARNING", "ERROR", "FATAL"};
fprintf(stderr, "[%s] %s:%d in %s: %s\n",
level_names[ctx->level],
ctx->file, ctx->line, ctx->function,
ctx->message);
if (ctx->errnum != 0) {
fprintf(stderr, "  System error: %s (errno=%d)\n",
strerror(ctx->errnum), ctx->errnum);
}
}
// Set custom error handler
void set_error_handler(ErrorHandler handler) {
global_handler = handler ? handler : default_error_handler;
}
// Push error onto stack
void push_error(ErrorLevel level, const char *file, const char *func,
int line, int errnum, const char *format, ...) {
ErrorContext *ctx = malloc(sizeof(ErrorContext));
if (!ctx) return;
ctx->level = level;
ctx->errnum = errnum;
strncpy(ctx->file, file, sizeof(ctx->file) - 1);
strncpy(ctx->function, func, sizeof(ctx->function) - 1);
ctx->line = line;
va_list args;
va_start(args, format);
vsnprintf(ctx->message, sizeof(ctx->message), format, args);
va_end(args);
ctx->next = error_stack;
error_stack = ctx;
// Call handler for non-debug errors
if (level >= ERROR_LEVEL_WARNING) {
ErrorHandler handler = global_handler ? global_handler : default_error_handler;
handler(ctx);
}
}
// Pop and report errors
void pop_error() {
if (error_stack) {
ErrorContext *ctx = error_stack;
error_stack = ctx->next;
free(ctx);
}
}
// Clear all errors
void clear_errors() {
while (error_stack) {
pop_error();
}
}
// Macros for easy error reporting
#define LOG_DEBUG(...) \
push_error(ERROR_LEVEL_DEBUG, __FILE__, __func__, __LINE__, 0, __VA_ARGS__)
#define LOG_INFO(...) \
push_error(ERROR_LEVEL_INFO, __FILE__, __func__, __LINE__, 0, __VA_ARGS__)
#define LOG_WARNING(...) \
push_error(ERROR_LEVEL_WARNING, __FILE__, __func__, __LINE__, errno, __VA_ARGS__)
#define LOG_ERROR(...) \
push_error(ERROR_LEVEL_ERROR, __FILE__, __func__, __LINE__, errno, __VA_ARGS__)
#define LOG_FATAL(...) \
do { \
push_error(ERROR_LEVEL_FATAL, __FILE__, __func__, __LINE__, errno, __VA_ARGS__); \
exit(EXIT_FAILURE); \
} while(0)
// Guard macro for function entry/exit
#define FUNCTION_ENTER() LOG_DEBUG("Entering %s", __func__)
#define FUNCTION_EXIT() LOG_DEBUG("Exiting %s", __func__)
// Example usage
int divide_numbers(int a, int b) {
FUNCTION_ENTER();
if (b == 0) {
LOG_ERROR("Division by zero attempted: %d / %d", a, b);
FUNCTION_EXIT();
return -1;
}
int result = a / b;
LOG_INFO("Division result: %d / %d = %d", a, b, result);
FUNCTION_EXIT();
return result;
}
void demonstrate_error_framework() {
set_error_handler(NULL);  // Use default handler
divide_numbers(10, 2);
divide_numbers(10, 0);  // This will log an error
clear_errors();
}

Debugging with errno

1. errno Tracing

#include <errno.h>
#include <stdio.h>
// Trace errno changes
void trace_errno(const char *file, int line, const char *func) {
static int last_errno = 0;
if (errno != last_errno) {
fprintf(stderr, "TRACE: %s:%d in %s - errno changed from %d (%s) to %d (%s)\n",
file, line, func,
last_errno, last_errno ? strerror(last_errno) : "none",
errno, errno ? strerror(errno) : "none");
last_errno = errno;
}
}
#define TRACE_ERRNO() trace_errno(__FILE__, __LINE__, __func__)
void function_with_operations() {
TRACE_ERRNO();
FILE *f = fopen("/nonexistent", "r");
TRACE_ERRNO();
if (f) fclose(f);
int *p = malloc(1000000000);  // May fail
TRACE_ERRNO();
free(p);
}

2. errno Assertions

#include <assert.h>
#include <errno.h>
#define ASSERT_ERRNO(expected) \
do { \
int _err = errno; \
if (_err != (expected)) { \
fprintf(stderr, "ASSERT_ERRNO failed at %s:%d: expected %d (%s), got %d (%s)\n", \
__FILE__, __LINE__, \
expected, expected ? strerror(expected) : "none", \
_err, _err ? strerror(_err) : "none"); \
assert(0); \
} \
} while(0)
// Example
void test_strtol() {
errno = 0;
long val = strtol("123abc", NULL, 10);
// strtol sets errno to 0 on success (no error)
ASSERT_ERRNO(0);
errno = 0;
val = strtol("999999999999999999999999", NULL, 10);
ASSERT_ERRNO(ERANGE);  // Should be out of range
}

Common Pitfalls and Solutions

1. Don't Check errno Without Checking Return Value

// WRONG
errno = 0;
fopen("file.txt", "r");
if (errno != 0) {
// This might trigger even if fopen succeeded!
perror("fopen");
}
// CORRECT
FILE *file = fopen("file.txt", "r");
if (file == NULL) {
int saved_errno = errno;  // Save immediately
fprintf(stderr, "fopen failed: %s\n", strerror(saved_errno));
} else {
fclose(file);
}

2. Don't Use errno After Other Functions

// WRONG
FILE *file = fopen("file.txt", "r");
if (file == NULL) {
printf("Error: ");  // printf may change errno!
perror("fopen");
}
// CORRECT
FILE *file = fopen("file.txt", "r");
if (file == NULL) {
int saved_errno = errno;  // Save immediately
printf("Error: %s\n", strerror(saved_errno));
} else {
fclose(file);
}

3. Preserve errno in Signal Handlers

#include <signal.h>
#include <errno.h>
#include <unistd.h>
void signal_handler(int sig) {
int saved_errno = errno;  // Save
// Do signal-safe operations
const char msg[] = "Signal received\n";
write(STDERR_FILENO, msg, sizeof(msg) - 1);
errno = saved_errno;  // Restore
}

4. Thread-Safe errno Access

// On modern systems, errno is thread-local, but be careful
// with library functions that may not be thread-safe
#include <pthread.h>
void* thread_func(void* arg) {
// Each thread has its own errno
errno = 0;
FILE *f = fopen("/nonexistent", "r");
if (f == NULL) {
// This errno is specific to this thread
printf("Thread %ld: %s\n", (long)pthread_self(), strerror(errno));
}
return NULL;
}

Complete Example: Robust File Processing

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#define BUFFER_SIZE 4096
#define MAX_RETRIES 3
typedef struct {
char *data;
size_t size;
int error;
int sys_errno;
} FileResult;
// Clear error state
void file_result_clear(FileResult *result) {
result->data = NULL;
result->size = 0;
result->error = 0;
result->sys_errno = 0;
}
// Read entire file with robust error handling
FileResult read_file_robust(const char *filename) {
FileResult result;
file_result_clear(&result);
int fd = -1;
char *buffer = NULL;
size_t total = 0;
size_t capacity = BUFFER_SIZE;
int retries = 0;
// Open file
fd = open(filename, O_RDONLY);
if (fd == -1) {
result.error = 1;
result.sys_errno = errno;
return result;
}
// Allocate initial buffer
buffer = malloc(capacity);
if (buffer == NULL) {
result.error = 1;
result.sys_errno = ENOMEM;
close(fd);
return result;
}
// Read loop with retry on interrupt
while (1) {
ssize_t bytes_read = read(fd, buffer + total, capacity - total);
if (bytes_read == -1) {
if (errno == EINTR && retries < MAX_RETRIES) {
retries++;
continue;
}
result.error = 1;
result.sys_errno = errno;
free(buffer);
close(fd);
return result;
}
if (bytes_read == 0) {
break;  // EOF
}
total += bytes_read;
retries = 0;  // Reset retries on successful read
// Expand buffer if needed
if (total == capacity) {
capacity *= 2;
char *new_buffer = realloc(buffer, capacity);
if (new_buffer == NULL) {
result.error = 1;
result.sys_errno = ENOMEM;
free(buffer);
close(fd);
return result;
}
buffer = new_buffer;
}
}
// Trim to actual size
char *final_buffer = realloc(buffer, total + 1);
if (final_buffer == NULL) {
final_buffer = buffer;  // Keep original if realloc fails
}
final_buffer[total] = '\0';
result.data = final_buffer;
result.size = total;
result.error = 0;
close(fd);
return result;
}
// Write file with fsync guarantee
int write_file_robust(const char *filename, const char *data, size_t size) {
int fd = -1;
ssize_t written;
size_t total = 0;
int ret = -1;
fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
fprintf(stderr, "Cannot open '%s': %s\n", filename, strerror(errno));
return -1;
}
while (total < size) {
written = write(fd, data + total, size - total);
if (written == -1) {
if (errno == EINTR) {
continue;
}
fprintf(stderr, "Write error: %s\n", strerror(errno));
goto cleanup;
}
total += written;
}
// Ensure data is on disk
if (fsync(fd) == -1) {
fprintf(stderr, "fsync error: %s\n", strerror(errno));
// Continue anyway - data may still be written
}
ret = 0;
cleanup:
close(fd);
return ret;
}
// Process a file with comprehensive error handling
int process_file(const char *input, const char *output) {
FileResult read_result;
int ret = -1;
// Read input
read_result = read_file_robust(input);
if (read_result.error) {
fprintf(stderr, "Failed to read '%s': ", input);
if (read_result.sys_errno) {
fprintf(stderr, "%s", strerror(read_result.sys_errno));
} else {
fprintf(stderr, "Unknown error");
}
fprintf(stderr, "\n");
return -1;
}
printf("Read %zu bytes from '%s'\n", read_result.size, input);
// Process data (simple transformation - convert to uppercase)
for (size_t i = 0; i < read_result.size; i++) {
if (read_result.data[i] >= 'a' && read_result.data[i] <= 'z') {
read_result.data[i] -= 32;
}
}
// Write output
if (write_file_robust(output, read_result.data, read_result.size) == -1) {
fprintf(stderr, "Failed to write to '%s'\n", output);
goto cleanup;
}
printf("Wrote %zu bytes to '%s'\n", read_result.size, output);
ret = 0;
cleanup:
free(read_result.data);
return ret;
}
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "Usage: %s <input> <output>\n", argv[0]);
return 1;
}
if (process_file(argv[1], argv[2]) == -1) {
return 1;
}
printf("File processed successfully\n");
return 0;
}

Best Practices Summary

PracticeWhy It Matters
Check return values before errnoerrno may be set even on success
Save errno immediatelyOther functions may change it
Clear errno before critical callsAvoid false positives
Use thread-local errnoModern systems are thread-safe
Provide context in error messagesInclude filename, operation details
Use perror() for quick debuggingSimple, standardized output
Handle EINTR properlySystem calls can be interrupted
Implement retry logicTransient errors can succeed later
Clean up resources on errorPrevent resource leaks
Document error conditionsHelp API users

Conclusion

Error handling with errno in C is a fundamental skill that separates novice from expert C programmers. While the mechanism is simple, using it effectively requires understanding:

  • Which functions set errno and how they indicate failure
  • Thread-safety considerations
  • Proper patterns for saving, checking, and restoring errno
  • Handling transient errors with retry logic
  • Building comprehensive error reporting frameworks

By following the patterns and practices outlined in this guide, you can build C applications that not only handle errors gracefully but also provide meaningful diagnostics for debugging and maintenance. Remember: robust error handling isn't an afterthought—it's a core design consideration that should be integrated from the earliest stages of development.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper