Error handling is often the most overlooked yet critical aspect of production C programming. Unlike modern languages with exceptions, C relies on a simple but powerful mechanism: errno. Understanding how to properly use errno can mean the difference between a robust application that gracefully handles failures and one that crashes mysteriously or corrupts data.
What is errno?
errno 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> extern int errno; // Actually defined in errno.h
Common errno Values
#include <errno.h> // Common error codes (not all, but most frequently encountered) #define EPERM 1 // Operation not permitted #define ENOENT 2 // No such file or directory #define ESRCH 3 // No such process #define EINTR 4 // Interrupted system call #define EIO 5 // I/O error #define ENXIO 6 // No such device or address #define E2BIG 7 // Argument list too long #define ENOEXEC 8 // Exec format error #define EBADF 9 // Bad file number #define ECHILD 10 // No child processes #define EAGAIN 11 // Try again #define ENOMEM 12 // Out of memory #define EACCES 13 // Permission denied #define EFAULT 14 // Bad address #define ENOTBLK 15 // Block device required #define EBUSY 16 // Device or resource busy #define EEXIST 17 // File exists #define EXDEV 18 // Cross-device link #define ENODEV 19 // No such device #define ENOTDIR 20 // Not a directory #define EISDIR 21 // Is a directory #define EINVAL 22 // Invalid argument #define ENFILE 23 // File table overflow #define EMFILE 24 // Too many open files #define ENOTTY 25 // Not a typewriter #define ETXTBSY 26 // Text file busy #define EFBIG 27 // File too large #define ENOSPC 28 // No space left on device #define ESPIPE 29 // Illegal seek #define EROFS 30 // Read-only file system #define EMLINK 31 // Too many links #define EPIPE 32 // Broken pipe #define EDOM 33 // Math argument out of domain of func #define ERANGE 34 // Math result not representable
Basic errno Usage Pattern
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main() {
FILE *file = fopen("nonexistent.txt", "r");
if (file == NULL) {
// Check errno immediately after failure
printf("Error opening file: %s\n", strerror(errno));
printf("Error code: %d\n", errno);
// Handle specific errors
switch (errno) {
case ENOENT:
printf("File doesn't exist. Creating default file...\n");
// Create default file
break;
case EACCES:
printf("Permission denied. Check file permissions.\n");
break;
case ENOMEM:
printf("Out of memory.\n");
exit(1);
default:
printf("Unknown error occurred.\n");
}
} else {
fclose(file);
}
return 0;
}
The perror() Function
perror() prints a descriptive error message to stderr:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
void demonstrate_perror() {
FILE *file = fopen("/root/secret.txt", "r");
if (file == NULL) {
// Prints: "Error opening file: Permission denied"
perror("Error opening file");
// perror is equivalent to:
// fprintf(stderr, "Error opening file: %s\n", strerror(errno));
}
}
Thread Safety and errno
In modern systems, errno is thread-local, making it safe to use in multithreaded programs:
#include <pthread.h>
#include <errno.h>
#include <stdio.h>
void* thread_function(void* arg) {
// Each thread has its own errno
FILE* file = fopen("/nonexistent", "r");
if (file == NULL) {
// This errno is specific to this thread
printf("Thread %ld: Error %d\n", (long)arg, errno);
}
return NULL;
}
int main() {
pthread_t thread1, thread2;
pthread_create(&thread1, NULL, thread_function, (void*)1);
pthread_create(&thread2, NULL, thread_function, (void*)2);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
return 0;
}
Advanced Error Handling Patterns
1. Clear errno Before Critical Operations
Always clear errno before calling functions that might set it:
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
long safe_strtol(const char* str, int base) {
char* endptr;
// Clear errno before call
errno = 0;
long result = strtol(str, &endptr, base);
// Check for errors
if (errno != 0) {
perror("strtol failed");
return 0;
}
if (endptr == str) {
fprintf(stderr, "No digits were found\n");
return 0;
}
return result;
}
2. Preserve errno in Signal Handlers
#include <signal.h>
#include <errno.h>
#include <stdio.h>
void signal_handler(int sig) {
// Save errno
int saved_errno = errno;
// Do signal-safe operations
write(STDERR_FILENO, "Signal received\n", 16);
// Restore errno
errno = saved_errno;
}
3. Wrapper Functions with Enhanced Error Reporting
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdarg.h>
typedef struct {
int code;
char message[256];
char function[64];
int line;
} ErrorInfo;
ErrorInfo last_error;
void set_error(int err_code, const char* func, int line, const char* fmt, ...) {
last_error.code = err_code;
last_error.line = line;
strncpy(last_error.function, func, sizeof(last_error.function) - 1);
va_list args;
va_start(args, fmt);
vsnprintf(last_error.message, sizeof(last_error.message), fmt, args);
va_end(args);
}
#define TRY(expr) \
do { \
errno = 0; \
if ((expr) < 0) { \
set_error(errno, __func__, __LINE__, "Failed at " #expr); \
return -1; \
} \
} while(0)
// Example usage
int copy_file(const char* src, const char* dst) {
FILE *source, *dest;
char buffer[4096];
size_t bytes;
TRY((long)(source = fopen(src, "rb")));
TRY((long)(dest = fopen(dst, "wb")));
while ((bytes = fread(buffer, 1, sizeof(buffer), source)) > 0) {
if (fwrite(buffer, 1, bytes, dest) != bytes) {
set_error(errno, __func__, __LINE__, "Write failed");
fclose(source);
fclose(dest);
return -1;
}
}
fclose(source);
fclose(dest);
return 0;
}
Comprehensive Error Handling Example
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
typedef enum {
ERROR_NONE = 0,
ERROR_FILE_OPEN,
ERROR_FILE_READ,
ERROR_FILE_WRITE,
ERROR_MEMORY,
ERROR_INVALID_PARAM
} ErrorCode;
typedef struct {
ErrorCode code;
int system_errno;
char message[512];
char file[64];
int line;
} DetailedError;
DetailedError g_error;
void clear_error() {
memset(&g_error, 0, sizeof(DetailedError));
}
void record_error(ErrorCode code, const char* file, int line, const char* format, ...) {
g_error.code = code;
g_error.system_errno = errno;
g_error.line = line;
strncpy(g_error.file, file, sizeof(g_error.file) - 1);
va_list args;
va_start(args, format);
vsnprintf(g_error.message, sizeof(g_error.message), format, args);
va_end(args);
}
#define RECORD_ERROR(code, ...) \
record_error(code, __FILE__, __LINE__, __VA_ARGS__)
void print_error() {
if (g_error.code == ERROR_NONE) return;
fprintf(stderr, "ERROR [%s:%d]: %s\n",
g_error.file, g_error.line, g_error.message);
if (g_error.system_errno != 0) {
fprintf(stderr, "System error: %s (errno=%d)\n",
strerror(g_error.system_errno), g_error.system_errno);
}
}
// Safe file reading function
char* read_file_safe(const char* filename, size_t* out_size) {
if (filename == NULL || out_size == NULL) {
RECORD_ERROR(ERROR_INVALID_PARAM, "NULL parameter");
return NULL;
}
clear_error();
// Open file
FILE* file = fopen(filename, "rb");
if (file == NULL) {
RECORD_ERROR(ERROR_FILE_OPEN, "Cannot open file '%s'", filename);
return NULL;
}
// Get file size
struct stat st;
if (fstat(fileno(file), &st) != 0) {
RECORD_ERROR(ERROR_FILE_READ, "Cannot stat file '%s'", filename);
fclose(file);
return NULL;
}
// Allocate buffer
char* buffer = (char*)malloc(st.st_size + 1);
if (buffer == NULL) {
RECORD_ERROR(ERROR_MEMORY, "Cannot allocate %ld bytes", st.st_size + 1);
fclose(file);
return NULL;
}
// Read file
size_t bytes_read = fread(buffer, 1, st.st_size, file);
if (bytes_read != (size_t)st.st_size) {
if (feof(file)) {
RECORD_ERROR(ERROR_FILE_READ, "Unexpected EOF reading '%s'", filename);
} else if (ferror(file)) {
RECORD_ERROR(ERROR_FILE_READ, "Error reading '%s'", filename);
}
free(buffer);
fclose(file);
return NULL;
}
buffer[st.st_size] = '\0';
*out_size = st.st_size;
fclose(file);
return buffer;
}
// Safe write function with retry
ssize_t safe_write(int fd, const void* buf, size_t count) {
size_t total_written = 0;
const char* ptr = (const char*)buf;
while (total_written < count) {
ssize_t written = write(fd, ptr + total_written, count - total_written);
if (written == -1) {
if (errno == EINTR) {
// Interrupted by signal, retry
continue;
} else if (errno == EAGAIN || errno == EWOULDBLOCK) {
// Non-blocking mode, would block
RECORD_ERROR(ERROR_FILE_WRITE, "Write would block");
return total_written;
} else {
// Real error
RECORD_ERROR(ERROR_FILE_WRITE, "Write failed: %s", strerror(errno));
return -1;
}
}
total_written += written;
}
return total_written;
}
Custom Error Handler with Callback
#include <setjmp.h>
typedef void (*ErrorHandler)(const char* file, int line, int err, const char* msg);
static ErrorHandler global_error_handler = NULL;
void register_error_handler(ErrorHandler handler) {
global_error_handler = handler;
}
void default_error_handler(const char* file, int line, int err, const char* msg) {
fprintf(stderr, "%s:%d: Error %d - %s\n", file, line, err, msg);
if (err != 0) {
fprintf(stderr, " System: %s\n", strerror(err));
}
}
#define CHECK(expr) \
do { \
if (!(expr)) { \
if (global_error_handler) { \
global_error_handler(__FILE__, __LINE__, errno, #expr); \
} \
return -1; \
} \
} while(0)
// Example with setjmp/longjmp for non-local exits
jmp_buf error_jmp;
void error_handler_longjmp(const char* file, int line, int err, const char* msg) {
fprintf(stderr, "Fatal error at %s:%d: %s\n", file, line, msg);
longjmp(error_jmp, err);
}
int risky_operation() {
if (setjmp(error_jmp) == 0) {
register_error_handler(error_handler_longjmp);
FILE* file = fopen("/nonexistent", "r");
CHECK(file != NULL);
// ... more operations
return 0;
} else {
// Error recovery
return -1;
}
}
errno in Library Development
When writing libraries, follow these best practices:
// mylibrary.h
#pragma once
#include <errno.h>
// Library-specific error codes
enum MyLibError {
MYLIB_SUCCESS = 0,
MYLIB_EINVAL = -1,
MYLIB_ENOMEM = -2,
MYLIB_EBUSY = -3,
MYLIB_EIO = -4
};
// Library functions should:
// 1. Preserve errno on success
// 2. Set errno appropriately on failure
// 3. Document which errors they set
/**
* Process data with error handling
* @param data Input data
* @param size Data size
* @return 0 on success, -1 on failure with errno set
*
* Errors:
* EINVAL - NULL data or invalid size
* ENOMEM - Memory allocation failed
* EBUSY - Resource temporarily unavailable
*/
int process_data(const void* data, size_t size);
// mylibrary.c
#include "mylibrary.h"
#include <stdlib.h>
#include <string.h>
int process_data(const void* data, size_t size) {
// Save original errno
int saved_errno = errno;
if (data == NULL || size == 0) {
errno = EINVAL;
return -1;
}
void* buffer = malloc(size);
if (buffer == NULL) {
errno = ENOMEM;
return -1;
}
memcpy(buffer, data, size);
// Simulate some processing
if (*(char*)buffer == 'x') {
free(buffer);
errno = EBUSY;
return -1;
}
free(buffer);
// Restore errno on success
errno = saved_errno;
return 0;
}
Thread-Safe Error Wrappers
#include <pthread.h>
// Thread-safe error context
typedef struct {
int errnum;
char message[256];
pthread_mutex_t lock;
} ThreadSafeError;
void ts_error_init(ThreadSafeError* err) {
pthread_mutex_init(&err->lock, NULL);
err->errnum = 0;
err->message[0] = '\0';
}
void ts_error_set(ThreadSafeError* err, int errnum, const char* msg) {
pthread_mutex_lock(&err->lock);
err->errnum = errnum;
strncpy(err->message, msg, sizeof(err->message) - 1);
pthread_mutex_unlock(&err->lock);
}
void ts_error_get(ThreadSafeError* err, int* errnum, char* buffer, size_t bufsize) {
pthread_mutex_lock(&err->lock);
*errnum = err->errnum;
strncpy(buffer, err->message, bufsize - 1);
pthread_mutex_unlock(&err->lock);
}
// Thread-local error storage
__thread int tls_errno;
__thread char tls_error_msg[256];
#define TLS_ERROR() \
do { \
tls_errno = errno; \
strncpy(tls_error_msg, strerror(errno), sizeof(tls_error_msg) - 1); \
} while(0)
#define TLS_CLEAR() \
do { \
tls_errno = 0; \
tls_error_msg[0] = '\0'; \
} while(0)
Debugging with errno
#include <assert.h>
// Debug macro that preserves errno
#define CHECK_ERRNO(expr) \
do { \
int saved_errno = errno; \
int result = (expr); \
errno = saved_errno; \
\
if (result < 0) { \
fprintf(stderr, "DEBUG: %s:%d: %s failed with errno=%d (%s)\n", \
__FILE__, __LINE__, #expr, errno, strerror(errno)); \
assert(0); \
} \
} while(0)
// Trace errno changes
void trace_errno(const char* func, int line) {
static int last_errno = 0;
if (errno != last_errno) {
printf("TRACE: %s:%d - errno changed from %d (%s) to %d (%s)\n",
func, line,
last_errno, last_errno ? strerror(last_errno) : "none",
errno, errno ? strerror(errno) : "none");
last_errno = errno;
}
}
#define TRACE_ERRNO() trace_errno(__func__, __LINE__)
Complete Example: Robust File Copy with Comprehensive Error Handling
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#define BUFFER_SIZE 8192
#define MAX_RETRIES 3
#define RETRY_DELAY_MS 100
typedef struct {
int source_fd;
int dest_fd;
char* buffer;
size_t buffer_size;
off_t total_bytes;
int retry_count;
} CopyContext;
typedef enum {
COPY_SUCCESS,
COPY_ERR_OPEN_SOURCE,
COPY_ERR_OPEN_DEST,
COPY_ERR_READ,
COPY_ERR_WRITE,
COPY_ERR_MEMORY,
COPY_ERR_INTERRUPTED
} CopyResult;
const char* copy_result_string(CopyResult result) {
switch(result) {
case COPY_SUCCESS: return "Success";
case COPY_ERR_OPEN_SOURCE: return "Cannot open source file";
case COPY_ERR_OPEN_DEST: return "Cannot open destination file";
case COPY_ERR_READ: return "Read error";
case COPY_ERR_WRITE: return "Write error";
case COPY_ERR_MEMORY: return "Memory allocation failed";
case COPY_ERR_INTERRUPTED: return "Operation interrupted";
default: return "Unknown error";
}
}
void log_error(const char* operation, const char* file, int line, int err) {
fprintf(stderr, "[ERROR] %s:%d: %s failed - %s (errno=%d)\n",
file, line, operation, strerror(err), err);
}
#define LOG_ERROR(op) log_error(op, __FILE__, __LINE__, errno)
CopyResult copy_file_with_retry(const char* source, const char* dest) {
CopyContext ctx = {
.source_fd = -1,
.dest_fd = -1,
.buffer = NULL,
.buffer_size = BUFFER_SIZE,
.total_bytes = 0,
.retry_count = 0
};
// Allocate buffer
ctx.buffer = (char*)malloc(ctx.buffer_size);
if (ctx.buffer == NULL) {
LOG_ERROR("malloc");
return COPY_ERR_MEMORY;
}
// Open source file
ctx.source_fd = open(source, O_RDONLY);
if (ctx.source_fd == -1) {
LOG_ERROR("open source");
free(ctx.buffer);
return COPY_ERR_OPEN_SOURCE;
}
// Open destination file
ctx.dest_fd = open(dest, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (ctx.dest_fd == -1) {
LOG_ERROR("open dest");
close(ctx.source_fd);
free(ctx.buffer);
return COPY_ERR_OPEN_DEST;
}
// Copy loop with retry logic
ssize_t bytes_read;
while ((bytes_read = read(ctx.source_fd, ctx.buffer, ctx.buffer_size)) > 0) {
ssize_t bytes_written = 0;
ssize_t total_written = 0;
int retries = 0;
while (total_written < bytes_read && retries < MAX_RETRIES) {
bytes_written = write(ctx.dest_fd,
ctx.buffer + total_written,
bytes_read - total_written);
if (bytes_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_MS * 1000);
continue;
} else {
// Real error
LOG_ERROR("write");
close(ctx.source_fd);
close(ctx.dest_fd);
free(ctx.buffer);
return COPY_ERR_WRITE;
}
}
total_written += bytes_written;
ctx.total_bytes += bytes_written;
retries = 0; // Reset retry count on successful write
}
if (total_written < bytes_read) {
fprintf(stderr, "Failed to write complete buffer after %d retries\n",
MAX_RETRIES);
close(ctx.source_fd);
close(ctx.dest_fd);
free(ctx.buffer);
return COPY_ERR_WRITE;
}
}
if (bytes_read == -1) {
if (errno != EINTR) {
LOG_ERROR("read");
close(ctx.source_fd);
close(ctx.dest_fd);
free(ctx.buffer);
return COPY_ERR_READ;
}
}
// Ensure data is written to disk
if (fsync(ctx.dest_fd) == -1) {
LOG_ERROR("fsync");
// Continue anyway - data might still be written
}
// Cleanup
close(ctx.source_fd);
close(ctx.dest_fd);
free(ctx.buffer);
printf("Successfully copied %ld bytes\n", ctx.total_bytes);
return COPY_SUCCESS;
}
int main(int argc, char* argv[]) {
if (argc != 3) {
fprintf(stderr, "Usage: %s <source> <destination>\n", argv[0]);
return 1;
}
CopyResult result = copy_file_with_retry(argv[1], argv[2]);
if (result != COPY_SUCCESS) {
fprintf(stderr, "Copy failed: %s\n", copy_result_string(result));
// Additional error details based on errno
if (errno != 0) {
fprintf(stderr, "System error details: %s\n", strerror(errno));
}
return 1;
}
printf("File copied successfully!\n");
return 0;
}
Best Practices Summary
- Check return values immediately - Always check function return values and examine errno right after a failure.
- Clear errno before critical calls - Set errno = 0 before calling functions that might set it.
- Use strerror() or perror() - Convert error codes to human-readable messages.
- Be thread-aware - Remember errno is thread-local in modern systems.
- Preserve errno in signal handlers - Save and restore errno in signal handlers.
- Document error conditions - Clearly document which errors your functions can return.
- Don't check errno for success - Only check errno after a function indicates failure.
- Use meaningful error messages - Include context (filename, operation) in error messages.
- Implement retry logic - For transient errors (EINTR, EAGAIN), implement appropriate retry logic.
- Clean up resources properly - Ensure resources are freed even when errors occur.
Common Pitfalls to Avoid
// WRONG - Don't do this
if (fopen("file.txt", "r") == NULL) {
printf("Error: %s\n", strerror(errno)); // errno might be overwritten by printf!
}
// CORRECT
FILE* file = fopen("file.txt", "r");
if (file == NULL) {
int saved_errno = errno; // Save immediately
printf("Error: %s\n", strerror(saved_errno));
}
// WRONG - Checking errno without verifying failure
errno = 0;
fopen("file.txt", "r");
if (errno != 0) { // Don't do this - function might succeed but set errno
// ...
}
// CORRECT
errno = 0;
FILE* file = fopen("file.txt", "r");
if (file == NULL && errno != 0) {
// Handle error
}
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 its nuances, thread-safety considerations, and integration with system calls and library functions.
The patterns and practices outlined in this guide provide a solid foundation for building robust C applications that gracefully handle failures, provide meaningful error messages, and maintain system stability even in adverse conditions. Remember that good error handling is not an afterthought—it's an integral part of software design that deserves as much attention as the "happy path" logic.