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
| Practice | Why It Matters |
|---|---|
| Check return values before errno | errno may be set even on success |
| Save errno immediately | Other functions may change it |
| Clear errno before critical calls | Avoid false positives |
| Use thread-local errno | Modern systems are thread-safe |
| Provide context in error messages | Include filename, operation details |
| Use perror() for quick debugging | Simple, standardized output |
| Handle EINTR properly | System calls can be interrupted |
| Implement retry logic | Transient errors can succeed later |
| Clean up resources on error | Prevent resource leaks |
| Document error conditions | Help 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.