File Error Handling
Master robust file error handling: errno, perror(), strerror(), file existence checks, permissions, disk space, and recovering from errors. Learn defensive programming for reliable file operations.
Understanding errno
errno is a global variable set by system calls and library functions when errors occur. It contains an error code indicating what went wrong. Always check function return values before examining errno. Reset errno before operations if you need reliable error detection.
#include <stdio.h>
#include <errno.h>
#include <string.h>
/* errno basics */
void errno_example(void) {
FILE *file = fopen("nonexistent.txt", "r");
if (file == NULL) {
/* errno set by fopen() */
printf("Error number: %d\n", errno);
/* Common errno values:
ENOENT - No such file or directory
EACCES - Permission denied
EMFILE - Too many open files
ENOMEM - Out of memory
ENOSPC - No space left on device
*/
if (errno == ENOENT) {
printf("File doesn't exist\n");
} else if (errno == EACCES) {
printf("Permission denied\n");
}
}
}
/* perror() - print error message */
void perror_example(void) {
FILE *file = fopen("test.txt", "r");
if (file == NULL) {
/* perror() prints: "prefix: error message" */
perror("Failed to open test.txt");
/* Output: Failed to open test.txt: No such file or directory */
return;
}
fclose(file);
}
/* strerror() - get error string */
void strerror_example(void) {
FILE *file = fopen("test.txt", "r");
if (file == NULL) {
int error = errno;
fprintf(stderr, "Error opening file: %s\n", strerror(error));
/* Output: Error opening file: No such file or directory */
return;
}
fclose(file);
}
/* Resetting errno */
void errno_reset_example(void) {
errno = 0; /* Reset before operation */
FILE *file = fopen("test.txt", "r");
if (file == NULL) {
if (errno != 0) {
/* Genuine error */
perror("fopen");
}
return;
}
fclose(file);
}
/* Comprehensive error checking */
int safe_open_file(const char *filename, const char *mode, FILE **file) {
errno = 0;
*file = fopen(filename, mode);
if (*file == NULL) {
int error = errno;
fprintf(stderr, "Cannot open %s (mode %s): %s\n",
filename, mode, strerror(error));
return -error;
}
return 0;
}
/* Usage */
void use_safe_open(void) {
FILE *file;
if (safe_open_file("data.txt", "r", &file) != 0) {
/* Handle error */
return;
}
/* Use file... */
fclose(file);
}Checking File Status
Before opening files, check if they exist, are readable/writable, or are directories. Use stat() or access() to query file properties. This prevents errors and enables better error messages.
#include <sys/stat.h>
#include <unistd.h>
/* Check if file exists */
int file_exists(const char *filename) {
struct stat st;
return stat(filename, &st) == 0;
}
/* Check if file is directory */
int is_directory(const char *path) {
struct stat st;
if (stat(path, &st) != 0) {
return 0;
}
return S_ISDIR(st.st_mode);
}
/* Check if file is regular file */
int is_regular_file(const char *path) {
struct stat st;
if (stat(path, &st) != 0) {
return 0;
}
return S_ISREG(st.st_mode);
}
/* Check if file is readable */
int is_readable(const char *filename) {
return access(filename, R_OK) == 0;
}
/* Check if file is writable */
int is_writable(const char *filename) {
return access(filename, W_OK) == 0;
}
/* Get file size */
long get_file_size(const char *filename) {
struct stat st;
if (stat(filename, &st) != 0) {
return -1;
}
return st.st_size;
}
/* Safe file opening with checks */
int open_file_safe(const char *filename, const char *mode, FILE **file) {
/* Check if opening for reading */
if (strchr(mode, 'r') != NULL) {
if (!file_exists(filename)) {
fprintf(stderr, "File '%s' does not exist\n", filename);
return -ENOENT;
}
if (!is_readable(filename)) {
fprintf(stderr, "File '%s' is not readable\n", filename);
return -EACCES;
}
if (is_directory(filename)) {
fprintf(stderr, "'%s' is a directory\n", filename);
return -EISDIR;
}
}
/* Check if opening for writing */
if (strchr(mode, 'w') != NULL || strchr(mode, 'a') != NULL) {
if (file_exists(filename) && !is_writable(filename)) {
fprintf(stderr, "File '%s' is not writable\n", filename);
return -EACCES;
}
}
/* Try to open */
*file = fopen(filename, mode);
if (*file == NULL) {
perror("fopen");
return -errno;
}
return 0;
}
/* Check available disk space (platform-specific) */
#ifdef __linux__
#include <sys/statvfs.h>
long long get_available_space(const char *path) {
struct statvfs stat;
if (statvfs(path, &stat) != 0) {
return -1;
}
/* Available space in bytes */
return (long long)stat.f_bavail * stat.f_frsize;
}
int has_enough_space(const char *path, long long required) {
long long available = get_available_space(path);
if (available < 0) {
return 0;
}
return available >= required;
}
#endif
/* File metadata */
typedef struct {
long size;
time_t modified;
int is_dir;
int is_readable;
int is_writable;
} FileInfo;
int get_file_info(const char *filename, FileInfo *info) {
struct stat st;
if (stat(filename, &st) != 0) {
return -1;
}
info->size = st.st_size;
info->modified = st.st_mtime;
info->is_dir = S_ISDIR(st.st_mode);
info->is_readable = (access(filename, R_OK) == 0);
info->is_writable = (access(filename, W_OK) == 0);
return 0;
}
void print_file_info(const char *filename) {
FileInfo info;
if (get_file_info(filename, &info) != 0) {
printf("Cannot get info for '%s'\n", filename);
return;
}
printf("File: %s\n", filename);
printf(" Size: %ld bytes\n", info.size);
printf(" Type: %s\n", info.is_dir ? "Directory" : "File");
printf(" Readable: %s\n", info.is_readable ? "Yes" : "No");
printf(" Writable: %s\n", info.is_writable ? "Yes" : "No");
printf(" Modified: %s", ctime(&info.modified));
}Robust Read/Write Error Handling
File operations can fail partway through. Check return values from every I/O operation. Distinguish EOF from errors using feof() and ferror(). Handle partial reads/writes. Implement retry logic for transient errors.
/* Safe read with full error checking */
int safe_read(FILE *file, void *buffer, size_t size, size_t *bytes_read) {
size_t result = fread(buffer, 1, size, file);
if (bytes_read != NULL) {
*bytes_read = result;
}
if (result < size) {
if (feof(file)) {
return 0; /* EOF reached */
}
if (ferror(file)) {
perror("Read error");
return -1;
}
}
return 0;
}
/* Safe write with full error checking */
int safe_write(FILE *file, const void *buffer, size_t size) {
size_t written = fwrite(buffer, 1, size, file);
if (written < size) {
if (ferror(file)) {
perror("Write error");
return -1;
}
}
return 0;
}
/* Read exact number of bytes or fail */
int read_exact(FILE *file, void *buffer, size_t size) {
size_t total = 0;
unsigned char *ptr = buffer;
while (total < size) {
size_t read_now = fread(ptr + total, 1, size - total, file);
if (read_now == 0) {
if (feof(file)) {
fprintf(stderr, "Unexpected EOF (read %zu of %zu bytes)\n",
total, size);
return -1;
}
if (ferror(file)) {
perror("Read error");
return -1;
}
}
total += read_now;
}
return 0;
}
/* Write exact number of bytes or fail */
int write_exact(FILE *file, const void *buffer, size_t size) {
size_t total = 0;
const unsigned char *ptr = buffer;
while (total < size) {
size_t written = fwrite(ptr + total, 1, size - total, file);
if (written == 0) {
if (ferror(file)) {
perror("Write error");
return -1;
}
}
total += written;
}
return 0;
}
/* Retry on transient errors */
#define MAX_RETRIES 3
int read_with_retry(FILE *file, void *buffer, size_t size) {
for (int attempt = 0; attempt < MAX_RETRIES; attempt++) {
clearerr(file); /* Clear error flags */
size_t bytes_read;
if (safe_read(file, buffer, size, &bytes_read) == 0) {
return bytes_read;
}
/* Check if error is recoverable */
if (errno == EINTR) { /* Interrupted system call */
continue; /* Retry */
}
break; /* Non-recoverable error */
}
return -1;
}
/* Copy file with error handling */
int copy_file_robust(const char *src, const char *dest) {
FILE *in = NULL;
FILE *out = NULL;
int result = -1;
/* Open source */
in = fopen(src, "rb");
if (in == NULL) {
fprintf(stderr, "Cannot open source '%s': %s\n",
src, strerror(errno));
goto cleanup;
}
/* Open destination */
out = fopen(dest, "wb");
if (out == NULL) {
fprintf(stderr, "Cannot create destination '%s': %s\n",
dest, strerror(errno));
goto cleanup;
}
/* Copy data */
char buffer[4096];
size_t bytes;
while ((bytes = fread(buffer, 1, sizeof(buffer), in)) > 0) {
if (fwrite(buffer, 1, bytes, out) != bytes) {
fprintf(stderr, "Write error to '%s': %s\n",
dest, strerror(errno));
goto cleanup;
}
}
/* Check for read error */
if (ferror(in)) {
fprintf(stderr, "Read error from '%s': %s\n",
src, strerror(errno));
goto cleanup;
}
/* Flush output */
if (fflush(out) != 0) {
fprintf(stderr, "Flush error: %s\n", strerror(errno));
goto cleanup;
}
result = 0; /* Success */
cleanup:
if (in != NULL) {
fclose(in);
}
if (out != NULL) {
if (fclose(out) != 0 && result == 0) {
fprintf(stderr, "Error closing '%s': %s\n",
dest, strerror(errno));
result = -1;
}
}
/* If failed, remove partial destination */
if (result != 0) {
remove(dest);
}
return result;
}Temporary Files and Atomic Operations
Temporary files enable safe file operations: write to temp, then rename. Renaming is atomic on POSIX systems - prevents partial writes if crash occurs. Use mkstemp() for secure temp files. Always clean up temps on error.
/* Create temporary file */
FILE* create_temp_file(char *template) {
/* template must end with "XXXXXX" */
int fd = mkstemp(template);
if (fd == -1) {
perror("mkstemp");
return NULL;
}
/* Convert file descriptor to FILE* */
FILE *file = fdopen(fd, "w+b");
if (file == NULL) {
close(fd);
unlink(template); /* Remove temp file */
return NULL;
}
return file;
}
/* Safe file write using temp file */
int write_file_atomic(const char *filename, const void *data, size_t size) {
char temp_template[256];
snprintf(temp_template, sizeof(temp_template), "%s.XXXXXX", filename);
/* Create temp file */
FILE *temp = create_temp_file(temp_template);
if (temp == NULL) {
return -1;
}
/* Write to temp */
if (fwrite(data, 1, size, temp) != size) {
perror("Write failed");
fclose(temp);
unlink(temp_template);
return -1;
}
/* Flush */
if (fflush(temp) != 0) {
perror("Flush failed");
fclose(temp);
unlink(temp_template);
return -1;
}
/* Close */
if (fclose(temp) != 0) {
perror("Close failed");
unlink(temp_template);
return -1;
}
/* Atomic rename */
if (rename(temp_template, filename) != 0) {
perror("Rename failed");
unlink(temp_template);
return -1;
}
return 0;
}
/* Update file safely */
int update_file_safe(const char *filename,
int (*update_func)(FILE*, void*),
void *user_data) {
char temp_file[256];
snprintf(temp_file, sizeof(temp_file), "%s.tmp", filename);
/* Open original */
FILE *input = fopen(filename, "rb");
if (input == NULL) {
perror("Cannot open input");
return -1;
}
/* Open temp for output */
FILE *output = fopen(temp_file, "wb");
if (output == NULL) {
perror("Cannot create temp");
fclose(input);
return -1;
}
/* Process */
int result = update_func(output, user_data);
fclose(input);
if (fclose(output) != 0 || result != 0) {
unlink(temp_file); /* Remove temp on error */
return -1;
}
/* Replace original */
if (rename(temp_file, filename) != 0) {
perror("Rename failed");
unlink(temp_file);
return -1;
}
return 0;
}
/* Backup before modifying */
int backup_and_modify(const char *filename,
int (*modify_func)(FILE*)) {
char backup[256];
snprintf(backup, sizeof(backup), "%s.backup", filename);
/* Create backup */
if (copy_file_robust(filename, backup) != 0) {
fprintf(stderr, "Backup failed\n");
return -1;
}
/* Modify file */
FILE *file = fopen(filename, "r+b");
if (file == NULL) {
fprintf(stderr, "Cannot open for modification\n");
return -1;
}
int result = modify_func(file);
if (fclose(file) != 0 || result != 0) {
/* Restore backup */
fprintf(stderr, "Modification failed, restoring backup\n");
remove(filename);
rename(backup, filename);
return -1;
}
/* Success - can remove backup or keep it */
// remove(backup);
return 0;
}
/* Lock file */
#ifdef __unix__
#include <fcntl.h>
int lock_file(FILE *file) {
int fd = fileno(file);
struct flock lock;
lock.l_type = F_WRLCK; /* Write lock */
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0; /* Entire file */
if (fcntl(fd, F_SETLK, &lock) == -1) {
if (errno == EACCES || errno == EAGAIN) {
fprintf(stderr, "File is locked by another process\n");
} else {
perror("fcntl");
}
return -1;
}
return 0;
}
int unlock_file(FILE *file) {
int fd = fileno(file);
struct flock lock;
lock.l_type = F_UNLCK; /* Unlock */
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
return fcntl(fd, F_SETLK, &lock);
}
#endifFile System Operations
Beyond opening files, you can create directories, remove files, rename, and check paths. These operations also require error handling. Understanding file system operations enables complete file management.
#include <sys/stat.h>
#include <dirent.h>
/* Remove file with error handling */
int remove_file_safe(const char *filename) {
if (!file_exists(filename)) {
fprintf(stderr, "'%s' does not exist\n", filename);
return -ENOENT;
}
if (remove(filename) != 0) {
fprintf(stderr, "Cannot remove '%s': %s\n",
filename, strerror(errno));
return -errno;
}
return 0;
}
/* Rename file safely */
int rename_file_safe(const char *oldname, const char *newname) {
if (!file_exists(oldname)) {
fprintf(stderr, "'%s' does not exist\n", oldname);
return -ENOENT;
}
if (file_exists(newname)) {
fprintf(stderr, "'%s' already exists\n", newname);
return -EEXIST;
}
if (rename(oldname, newname) != 0) {
fprintf(stderr, "Cannot rename '%s' to '%s': %s\n",
oldname, newname, strerror(errno));
return -errno;
}
return 0;
}
/* Create directory */
int create_directory(const char *path) {
#ifdef _WIN32
if (mkdir(path) != 0) {
#else
if (mkdir(path, 0755) != 0) {
#endif
if (errno == EEXIST) {
/* Already exists */
return 0;
}
fprintf(stderr, "Cannot create directory '%s': %s\n",
path, strerror(errno));
return -errno;
}
return 0;
}
/* Create directory recursively */
int create_directory_recursive(const char *path) {
char tmp[256];
char *p = NULL;
size_t len;
snprintf(tmp, sizeof(tmp), "%s", path);
len = strlen(tmp);
if (tmp[len - 1] == '/') {
tmp[len - 1] = 0;
}
for (p = tmp + 1; *p; p++) {
if (*p == '/') {
*p = 0;
if (create_directory(tmp) != 0 && errno != EEXIST) {
return -1;
}
*p = '/';
}
}
return create_directory(tmp);
}
/* List directory contents */
int list_directory(const char *path) {
DIR *dir = opendir(path);
if (dir == NULL) {
fprintf(stderr, "Cannot open directory '%s': %s\n",
path, strerror(errno));
return -errno;
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
/* Skip . and .. */
if (strcmp(entry->d_name, ".") == 0 ||
strcmp(entry->d_name, "..") == 0) {
continue;
}
printf("%s\n", entry->d_name);
}
closedir(dir);
return 0;
}
/* Check if directory is empty */
int is_directory_empty(const char *path) {
DIR *dir = opendir(path);
if (dir == NULL) {
return -1;
}
struct dirent *entry;
int count = 0;
while ((entry = readdir(dir)) != NULL) {
if (strcmp(entry->d_name, ".") != 0 &&
strcmp(entry->d_name, "..") != 0) {
count++;
break;
}
}
closedir(dir);
return count == 0;
}
/* Remove directory recursively */
int remove_directory_recursive(const char *path) {
DIR *dir = opendir(path);
if (dir == NULL) {
return -1;
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 ||
strcmp(entry->d_name, "..") == 0) {
continue;
}
char full_path[512];
snprintf(full_path, sizeof(full_path), "%s/%s", path, entry->d_name);
if (is_directory(full_path)) {
remove_directory_recursive(full_path);
} else {
remove(full_path);
}
}
closedir(dir);
return rmdir(path);
}Summary & What's Next
Key Takeaways:
- ✅ Always check return values of file operations
- ✅ Use errno, perror(), and strerror() for errors
- ✅ Check file existence and permissions before opening
- ✅ Distinguish EOF from errors with feof() and ferror()
- ✅ Use temporary files for atomic updates
- ✅ Clean up partial files on errors
- ✅ Implement retry logic for transient errors
- ✅ Use file locking for concurrent access