Senior 7 min · March 06, 2026

File Handling in C — 'w' Mode Truncated Our Logs

A daemon restart with fopen("daemon.log", "w") wiped 2 hours of logs.

N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Everything here is grounded in real deployments.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • C file I/O uses FILE* pointers for buffered access to disk files
  • fopen() opens a file; always check for NULL before using the pointer
  • fread() and fwrite() handle raw binary data with fixed-size records
  • fseek() provides O(1) random access when combined with sizeof arithmetic
  • fclose() flushes buffers and releases the file descriptor — skipping it loses data
  • Text mode ('r','w') and binary mode ('rb','wb') differ in newline translation on Windows
✦ Definition~90s read
What is File Handling in C?

File handling in C is the mechanism for reading from and writing to files on disk using the standard I/O library (stdio.h). At its core, it wraps operating system file descriptors into a FILE* stream abstraction, buffering data for performance and providing portable functions like fopen, fprintf, fread, and fclose.

Imagine your program's memory is a whiteboard — fast, but erased the moment you walk out of the room.

The critical trap—and the reason this article exists—is that fopen with mode "w" truncates an existing file to zero length the moment it opens, silently destroying any prior content. This is by design: "w" means "write from the start, discarding everything." If you need to append or modify without wiping data, you must use "a" (append) or "r+" (read/write without truncation).

In the broader ecosystem, C's file handling sits between low-level POSIX calls (open, read, write) and higher-level abstractions like C++ fstream or Python's open(). You'd use C file I/O in embedded systems, kernel modules, or performance-critical applications where you need deterministic control over buffering and system calls.

Avoid it when you need automatic resource management, Unicode encoding, or structured serialization—those are better served by libraries like SQLite, Protocol Buffers, or language-specific runtimes. The FILE* lifecycle is manual: open, check for NULL, read/write, then close—forgetting fclose leaks file descriptors and buffers, a common source of "too many open files" crashes in production.

Real-world consequences are brutal. A misused "w" mode in a logging daemon can truncate months of audit trails in microseconds. Companies like Cloudflare and Stripe have documented outages from similar file-handling bugs in C-based infrastructure. The safe patterns—always check fopen return values, use "a" for logs, validate fread/fwrite return counts, and wrap file operations in error-handling macros—are non-negotiable for production code.

This article walks through each mode, the buffer flush semantics, and how to use fseek/ftell for random access without corrupting your data.

Plain-English First

Imagine your program's memory is a whiteboard — fast, but erased the moment you walk out of the room. A file is a notebook you leave on the desk: it survives after you leave, and anyone (or any program) can open it later and pick up exactly where you left off. File handling in C is just the set of tools that lets your program open that notebook, read what's written, add new pages, and close it properly when it's done. Forget to close it and it's like leaving the notebook soaking in the rain — you risk losing everything.

Every meaningful program eventually needs to outlive itself. A configuration it reads on startup, a log it writes during a crash, a CSV it exports for a client — none of that works if data only lives in RAM. File handling is the bridge between your program's fleeting in-memory world and the persistent world on disk. That's not a nice-to-have feature; it's table stakes for anything production-grade.

The C standard library solves this with a thin but powerful abstraction: the FILE pointer. Instead of dealing with raw OS system calls (which differ between Linux, Windows, and macOS), you get a portable, consistent API — fopen, fread, fwrite, fprintf, fscanf, fclose — that works identically everywhere C runs. The OS complexity is hidden behind that single FILE* handle, which is exactly why C file I/O has survived for fifty years without being replaced.

By the end of this article you'll be able to open files safely, read and write both text and binary data, handle errors the way real production code does, and avoid the three mistakes that trip up even experienced developers. You'll also walk away with the conceptual model that makes every fread/fwrite call feel obvious rather than magical.

What File Handling in C Actually Does — and Why 'w' Mode Destroys Your Data

File handling in C is the set of standard library functions (fopen, fclose, fprintf, fscanf, etc.) that allow a program to read from and write to persistent storage via a FILE* stream. The core mechanic is that fopen returns a pointer to a FILE structure that holds the stream state, buffer, and file descriptor; you then use that pointer for all subsequent I/O operations until fclose flushes the buffer and releases the resource.

In practice, the mode string you pass to fopen determines the initial state of the file pointer and whether the file is truncated or appended. 'w' opens the file for writing, creates it if it doesn't exist, and — critically — truncates the file to zero length immediately upon success. This happens before your first fprintf call. There is no undo. The file's previous content is gone the moment fopen returns. 'a' (append) does not truncate; 'r+' opens for read/write without truncation but requires the file to exist.

Use 'w' only when you intend to replace the entire file content from scratch — for example, writing a fresh configuration file or a new log file per run. Never use 'w' for appending logs, updating records in place, or any scenario where existing data must survive. In production systems, a single 'w' where 'a' was intended can silently erase hours of critical log data, making this one of the most common and costly C I/O mistakes.

Truncation Is Immediate and Silent
fopen with 'w' truncates the file before your program writes a single byte — there is no confirmation, no backup, and no way to recover the lost data from within the process.
Production Insight
A monitoring daemon rotated logs by reopening the file with 'w' instead of 'a', wiping the current log file every 5 minutes — the team only noticed after 3 hours of missing incident data.
Symptom: log files consistently showed only the last few seconds of data, and file sizes were always small despite high traffic.
Rule of thumb: always use 'a' for log files and 'r+' for in-place updates; reserve 'w' exclusively for cases where you explicitly want to replace the entire file.
Key Takeaway
fopen('w') truncates the file at open time, not at first write — data loss is immediate and silent.
Use 'a' for appending, 'r+' for read/write without truncation, and 'w' only when you intend to replace the entire file.
Always validate the mode string in code reviews — a single character change can destroy production data.
File Handling in C — 'w' Mode Truncated Our Logs THECODEFORGE.IO File Handling in C — 'w' Mode Truncated Our Logs Flow from file open to safe I/O patterns with mode warnings File Opening Modes 'w' truncates existing file; 'a' appends FILE* Lifecycle fopen → operations → fclose Text I/O Functions fprintf, fscanf for formatted data Binary I/O Functions fread, fwrite for structs and raw data Random Access fseek, ftell for file positioning Safe File I/O Check fopen return; handle errors ⚠ 'w' mode silently truncates existing file on open Always verify mode; use 'a' or 'r+' to preserve data THECODEFORGE.IO
thecodeforge.io
File Handling in C — 'w' Mode Truncated Our Logs
File Handling C

Opening and Closing Files — The FILE* Lifecycle You Must Respect

Every file operation in C starts with fopen() and must end with fclose(). The FILE* pointer you get back isn't a file — it's a control structure the C runtime uses to buffer data, track your position, and manage the connection to the OS file descriptor. Think of it as a library card: you need it to check out books, and you must return it when you're done.

fopen() takes two arguments: the file path and a mode string. The mode tells the OS what you intend to do — 'r' for reading an existing file, 'w' for writing (creating or truncating), 'a' for appending, and their binary variants 'rb', 'wb', 'ab'. Choosing the wrong mode is one of the most common sources of data loss in C programs.

fopen() returns NULL on failure — network drive unavailable, wrong permissions, file doesn't exist in 'r' mode. You must check for NULL before doing anything else. Skipping that check is not a style issue; it's a segfault waiting to happen. Always pair your NULL check with perror() so the error message tells you exactly what went wrong, not just that something did.

file_lifecycle.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <stdlib.h>  // for exit()

int main(void) {
    // Attempt to open (or create) a log file for writing
    // 'w' mode: creates the file if it doesn't exist,
    //           TRUNCATES it to zero bytes if it does — be careful!
    FILE *log_file = fopen("app_log.txt", "w");

    // ALWAYS check for NULL before using the pointer.
    // fopen returns NULL when the file can't be opened for any reason.
    if (log_file == NULL) {
        // perror prints the system error message (e.g. "Permission denied")
        // prefixed by whatever string you pass it.
        perror("ERROR: Could not open app_log.txt");
        exit(EXIT_FAILURE);  // exit cleanly rather than crash later
    }

    // Write a header line to the log
    fprintf(log_file, "=== Application Log Started ===\n");
    fprintf(log_file, "Status: Initialised successfully\n");

    // fclose flushes any buffered data to disk AND releases the FILE*
    // resource. Skip this and you may lose the last chunk of data.
    if (fclose(log_file) != 0) {
        perror("ERROR: Could not close app_log.txt");
        exit(EXIT_FAILURE);
    }

    printf("Log written successfully. Check app_log.txt\n");
    return 0;
}
Output
Log written successfully. Check app_log.txt
[Contents of app_log.txt]
=== Application Log Started ===
Status: Initialised successfully
Watch Out: 'w' Mode Silently Destroys Your File
Opening an existing file with 'w' immediately truncates it to zero bytes — before you write a single character. If you want to add to an existing file without destroying it, use 'a' (append) mode. This mistake has wiped production log files and cost engineers their evenings.
Production Insight
Forgetting the NULL check on fopen leads to segfault when the code dereferences the FILE*.
In production, a missing config file or full disk silently crashes the process.
Rule: always pair fopen with perror() and exit — not printf("Error").
Key Takeaway
fopen + fclose is a resource lifecycle, like malloc/free.
Never code a path where fopen succeeds but fclose is skipped.
Last line: 'w' mode is destructive — verify intent before using it.

Reading and Writing Text Files — fprintf, fscanf, fgets in Practice

Text file I/O is the most common form of file handling you'll write. Configuration files, CSV exports, log files, INI parsers — they're all text. C gives you three main tools: fprintf/fscanf (formatted, mirrors printf/scanf), fgets (line-by-line reading, the safe choice), and fputc/fgetc (character by character, useful for parsers).

fscanf looks tempting but has a reputation for being fragile — it stops at whitespace, which means names with spaces silently break it. fgets is almost always the better choice for reading: it reads an entire line up to a newline or the buffer limit, whichever comes first, so it never overflows your buffer (unlike gets, which was removed from C11 for that exact reason).

The pattern for reading a text file line by line is a while loop with fgets as the condition. When fgets hits end-of-file it returns NULL, ending the loop cleanly. Inside the loop you can parse, tokenise, or process each line however you need. This pattern powers everything from simple config readers to lightweight CSV parsers in embedded systems.

text_file_rw.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_LINE_LENGTH 256
#define SCORES_FILE     "scores.txt"

// Writes three player scores to a text file
void write_scores(void) {
    FILE *scores_file = fopen(SCORES_FILE, "w");
    if (scores_file == NULL) {
        perror("ERROR: Cannot create scores file");
        exit(EXIT_FAILURE);
    }

    // fprintf works exactly like printf but targets a file
    fprintf(scores_file, "Alice,4200\n");
    fprintf(scores_file, "Bob,3875\n");
    fprintf(scores_file, "Charlie,5100\n");

    fclose(scores_file);
    printf("Scores written to %s\n", SCORES_FILE);
}

// Reads every line back and prints it with a rank number
void read_scores(void) {
    FILE *scores_file = fopen(SCORES_FILE, "r");
    if (scores_file == NULL) {
        perror("ERROR: Cannot open scores file for reading");
        exit(EXIT_FAILURE);
    }

    char line_buffer[MAX_LINE_LENGTH];  // holds one line at a time
    int rank = 1;

    printf("\n--- Leaderboard ---\n");

    // fgets reads up to (MAX_LINE_LENGTH - 1) chars, stops at '\n' or EOF.
    // It returns NULL when there's nothing left to read.
    while (fgets(line_buffer, sizeof(line_buffer), scores_file) != NULL) {
        // fgets keeps the trailing '\n' — strip it for clean display
        line_buffer[strcspn(line_buffer, "\n")] = '\0';

        // strtok splits the line on the comma delimiter
        char *player_name  = strtok(line_buffer, ",");
        char *score_string = strtok(NULL, ",");

        if (player_name != NULL && score_string != NULL) {
            printf("  #%d  %-10s  %s pts\n", rank++, player_name, score_string);
        }
    }

    fclose(scores_file);
}

int main(void) {
    write_scores();
    read_scores();
    return 0;
}
Output
Scores written to scores.txt
--- Leaderboard ---
#1 Alice 4200 pts
#2 Bob 3875 pts
#3 Charlie 5100 pts
Pro Tip: Strip the Newline fgets Leaves Behind
fgets includes the '\n' character in the buffer. Use line_buffer[strcspn(line_buffer, "\n")] = '\0' to strip it cleanly — this is safer than manually indexing strlen()-1, which breaks on empty lines.
Production Insight
fscanf silently misparses lines with spaces or commas — it's a frequent source of config file corruption.
In production, a single malformed line can cause the entire file to be misread.
Rule: use fgets + sscanf or strtok for all structured text parsing.
Key Takeaway
fgets is safer than fscanf for reading lines.
Always strip the trailing newline after fgets.
Last line: never trust the file format — parse defensively.

Binary File I/O — fread and fwrite for Structs and Raw Data

Text files are human-readable but inefficient for structured data. If you're storing a thousand employee records, writing each integer as a string of ASCII digits wastes space and requires parsing on the way back in. Binary mode writes the raw bytes from memory directly to disk — what's in the struct goes in, what's on disk comes back out. Same layout, no conversion.

fwrite(pointer, size_of_one_element, count, file) is the workhorse. It returns the number of elements successfully written — if that's less than count, something went wrong and you need to check ferror(). fread mirrors it exactly: same four arguments, returns how many elements it actually read.

The critical thing to understand is that binary files are not portable across different CPU architectures. A file written on a little-endian x86 machine will have its integer bytes in reverse order compared to what a big-endian SPARC machine expects. For embedded systems or single-platform tools this doesn't matter. For cross-platform data exchange, use text or a defined binary format like protobuf. Know this tradeoff before you choose binary.

binary_file_rw.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define RECORDS_FILE "employees.dat"
#define MAX_NAME_LEN 50

// A fixed-size struct — perfect for binary I/O because the layout is predictable
typedef struct {
    int    employee_id;
    char   full_name[MAX_NAME_LEN];
    double annual_salary;
} Employee;

void save_employees(const Employee *employees, int count) {
    // 'wb' — write binary. Without the 'b', Windows may corrupt data
    // by translating '\n' bytes in your struct to '\r\n'.
    FILE *data_file = fopen(RECORDS_FILE, "wb");
    if (data_file == NULL) {
        perror("ERROR: Cannot open employees.dat for writing");
        exit(EXIT_FAILURE);
    }

    // fwrite returns the number of items written.
    // If it doesn't match 'count', the disk might be full.
    size_t items_written = fwrite(employees, sizeof(Employee), count, data_file);
    if ((int)items_written != count) {
        fprintf(stderr, "ERROR: Only wrote %zu of %d records\n", items_written, count);
        fclose(data_file);
        exit(EXIT_FAILURE);
    }

    fclose(data_file);
    printf("Saved %d employee record(s) to %s\n", count, RECORDS_FILE);
}

void load_and_print_employees(int expected_count) {
    FILE *data_file = fopen(RECORDS_FILE, "rb");  // 'rb' — read binary
    if (data_file == NULL) {
        perror("ERROR: Cannot open employees.dat for reading");
        exit(EXIT_FAILURE);
    }

    Employee record;  // reuse a single struct for each read

    printf("\n--- Employee Records ---\n");

    // Read one Employee at a time until fread returns 0 (EOF or error)
    int rank = 1;
    while (fread(&record, sizeof(Employee), 1, data_file) == 1) {
        printf("  [%d] ID: %04d  Name: %-20s  Salary: $%.2f\n",
               rank++,
               record.employee_id,
               record.full_name,
               record.annual_salary);
    }

    // Distinguish between EOF (normal) and a real read error
    if (ferror(data_file)) {
        perror("ERROR: Failed while reading employees.dat");
    }

    fclose(data_file);
}

int main(void) {
    Employee team[3] = {
        {1001, "Sarah Mitchell",  92500.00},
        {1002, "James Okafor",   87000.00},
        {1003, "Lin Wei",       105000.00}
    };

    save_employees(team, 3);
    load_and_print_employees(3);

    return 0;
}
Output
Saved 3 employee record(s) to employees.dat
--- Employee Records ---
[1] ID: 1001 Name: Sarah Mitchell Salary: $92500.00
[2] ID: 1002 Name: James Okafor Salary: $87000.00
[3] ID: 1003 Name: Lin Wei Salary: $105000.00
Watch Out: Struct Padding Can Corrupt Binary Files
The compiler may insert padding bytes between struct members for alignment. Two builds with different compiler flags can produce structs of different sizes — meaning a file written by one binary can't be read correctly by another. Use __attribute__((packed)) on GCC or #pragma pack(1) on MSVC if cross-version portability matters, but be aware this can hurt performance.
Production Insight
A binary file written on one system may be unreadable on another due to padding or endianness.
A dev team using different compilers for unit tests vs production can silently corrupt data.
Rule: always run a round-trip test (write then read) on every target platform.
Key Takeaway
Binary I/O is fast but fragile — bytes are bytes.
Pack structs when portability matters, but test for performance impact.
Last line: always use 'b' in mode for binary data — especially on Windows.

Random Access and File Positioning — fseek, ftell and rewind

Sequential reading is fine for logs and config files, but a database that re-reads the whole file to update one record is a performance disaster. C gives you fseek() for jumping to any byte position, ftell() for finding out where you currently are, and rewind() as a shortcut for jumping back to the start.

fseek(file, offset, origin) takes three arguments. The origin can be SEEK_SET (from the start of the file), SEEK_CUR (relative to current position), or SEEK_END (relative to the end — useful for finding the file size). The offset is a signed long, so you can move forward or backward.

The real power of fseek emerges with fixed-size structs. Because every Employee record is exactly sizeof(Employee) bytes, you can jump to the nth record with fseek(file, n * sizeof(Employee), SEEK_SET). This is how simple flat-file databases work — and understanding this pattern demystifies a lot of file-based data storage you'll encounter in embedded systems and legacy codebases.

random_access.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define RECORDS_FILE "employees.dat"  // reuses file from previous example
#define MAX_NAME_LEN 50

typedef struct {
    int    employee_id;
    char   full_name[MAX_NAME_LEN];
    double annual_salary;
} Employee;

// Returns file size in bytes using fseek/ftell — a classic C idiom
long get_file_size(FILE *file) {
    fseek(file, 0, SEEK_END);    // jump to the very end
    long size = ftell(file);     // ftell returns current byte position
    rewind(file);                // rewind back to the start for subsequent reads
    return size;
}

// Update a single record by index without touching other records
void update_salary(int record_index, double new_salary) {
    // 'r+b' = read-write binary (file must already exist)
    FILE *data_file = fopen(RECORDS_FILE, "r+b");
    if (data_file == NULL) {
        perror("ERROR: Cannot open employees.dat for update");
        exit(EXIT_FAILURE);
    }

    long file_size    = get_file_size(data_file);
    int  total_records = (int)(file_size / sizeof(Employee));

    printf("File size: %ld bytes | Records: %d\n", file_size, total_records);

    if (record_index < 0 || record_index >= total_records) {
        fprintf(stderr, "ERROR: Record index %d out of range (0-%d)\n",
                record_index, total_records - 1);
        fclose(data_file);
        exit(EXIT_FAILURE);
    }

    // Jump directly to the target record — no need to read what's before it
    long byte_offset = (long)record_index * sizeof(Employee);
    fseek(data_file, byte_offset, SEEK_SET);

    // Read the current record first so we can update just the salary field
    Employee target_record;
    if (fread(&target_record, sizeof(Employee), 1, data_file) != 1) {
        perror("ERROR: Failed to read target record");
        fclose(data_file);
        exit(EXIT_FAILURE);
    }

    printf("Updating %s: $%.2f -> $%.2f\n",
           target_record.full_name,
           target_record.annual_salary,
           new_salary);

    target_record.annual_salary = new_salary;

    // Seek back to the same position — fread advanced the pointer past the record
    fseek(data_file, byte_offset, SEEK_SET);
    fwrite(&target_record, sizeof(Employee), 1, data_file);

    fclose(data_file);
    printf("Update complete.\n");
}

int main(void) {
    // Update the salary of the second employee (index 1 = James Okafor)
    update_salary(1, 95000.00);
    return 0;
}
Output
File size: 348 bytes | Records: 3
Updating James Okafor: $87000.00 -> $95000.00
Update complete.
Interview Gold: How to Find File Size in C
The fseek(SEEK_END) + ftell() + rewind() trio is the standard C idiom for getting a file's size, since C has no dedicated file-size function in the standard library. Interviewers love asking this — and the follow-up is whether this works on binary vs text files (it's only guaranteed accurate for binary mode).
Production Insight
fseek on text files on Windows produces unreliable offsets due to newline translation.
A record update in a text file may accidentally overwrite part of another record.
Rule: use binary mode for any random access that relies on byte offsets.
Key Takeaway
fseek + sizeof = O(1) access — the foundation of flat-file databases.
Always use binary mode for random access.
Last line: ftell after fseek(SEEK_END) is the standard file size idiom.

Error Handling and Safe File I/O Patterns for Production Code

Production file I/O isn't just about reading and writing — it's about handling failures gracefully. fread and fwrite return the number of items actually transferred, which may be less than the count you asked for. You must check these returns and then call ferror() to distinguish between an end-of-file condition and a genuine I/O error. feof() tests for end-of-file but is only valid after a failed read — don't use it to predict future reads.

Buffering is another hidden concern. The C runtime buffers output internally and only flushes when the buffer fills or fclose is called. setvbuf() lets you control the buffer size or disable buffering entirely, which is critical for writing logs in crash-prone environments. For production wrappers, use a helper function that combines fopen, error checking, and setvbuf into a single call — this reduces the surface area for mistakes.

Finally, remember that the file system is a shared resource. File locks (via flock or fcntl) become necessary when multiple processes write to the same file. C's standard I/O library doesn't provide locking, so you must use OS-specific calls or external coordination.

io_thecodeforge_safe_file.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

// Production-safe fopen with buffer control and explicit error reporting
// Returns NULL on failure after printing a meaningful error message.
// If setbuf_size > 0, uses a custom buffer; if 0, unbuffered.
FILE *io_thecodeforge_file_open(const char *path, const char *mode, size_t setbuf_size) {
    FILE *fp = fopen(path, mode);
    if (fp == NULL) {
        fprintf(stderr, "[io_thecodeforge] ERROR: fopen(\"%s\", \"%s\") failed: %s\n",
                path, mode, strerror(errno));
        return NULL;
    }

    if (setbuf_size > 0) {
        char *buf = (char *)malloc(setbuf_size);
        if (buf == NULL) {
            // setvbuf with NULL buffer is allowed — just use default
            fprintf(stderr, "[io_thecodeforge] WARNING: malloc(%zu) failed, using default buffer\n", setbuf_size);
        } else {
            if (setvbuf(fp, buf, _IOFBF, setbuf_size) != 0) {
                fprintf(stderr, "[io_thecodeforge] WARNING: setvbuf failed, using default buffer\n");
                free(buf);
            }
        }
    } else if (setbuf_size == 0) {
        setvbuf(fp, NULL, _IONBF, 0);  // unbuffered
    }

    return fp;
}

// Safely write exactly 'count' items of 'size' bytes, retrying on short writes
int io_thecodeforge_file_write(FILE *fp, const void *data, size_t size, size_t count) {
    size_t total_written = 0;
    const unsigned char *ptr = (const unsigned char *)data;
    size_t total_bytes = size * count;

    while (total_written < total_bytes) {
        size_t written = fwrite(ptr + total_written, 1, total_bytes - total_written, fp);
        if (written == 0) {
            if (ferror(fp)) {
                fprintf(stderr, "[io_thecodeforge] ERROR: fwrite failed: %s\n", strerror(errno));
                return -1;
            }
            // Shouldn't happen with blocking I/O, but guard against infinite loop
            break;
        }
        total_written += written;
    }

    return (total_written == total_bytes) ? 0 : -1;
}

// Usage example
int main(void) {
    FILE *log = io_thecodeforge_file_open("production.log", "a", 8192);
    if (log == NULL) {
        exit(EXIT_FAILURE);
    }

    const char *msg = "Service started successfully\n";
    if (io_thecodeforge_file_write(log, msg, 1, strlen(msg)) != 0) {
        fprintf(stderr, "Failed to write startup message\n");
    }

    fclose(log);
    return 0;
}
Output
No output (success) — but check production.log for appended line.
Mental Model: File Operations Are System Calls, Not Magic
  • fopen can fail: file doesn't exist, permissions, full disk, network mount down.
  • fwrite can fail partway through: disk full, quota exceeded, interrupted.
  • fclose is the only way to guarantee data has been flushed to disk.
  • Multiple processes writing to one file need external locking — C's stdio doesn't provide it.
  • Error recovery should be part of the design, not an afterthought.
Production Insight
A crash mid-write without fclose leaves the file in an unknown state — partial writes are common.
Production logging libraries use unbuffered writes to ensure every line is immediately written.
Rule: use setvbuf(_IONBF) for logs, or call fflush after every critical write.
Key Takeaway
Wrap fopen/fread/fwrite/fclose in safe helpers to centralise error handling.
Always check return values — partial writes are real.
Last line: in production, unbuffered writes or explicit fflush prevent data loss.

File Opening Modes — The Silent Data Killer Is ios::trunc

Every file open call carries a mode. No mode means defaults. And defaults will destroy your data without warning.

When you open an ofstream without specifying a mode, C++ applies ios::out | ios::trunc by default. That trunc flag erases the file's existing content the moment the stream constructs. One misconfigured constructor, and your customer's configuration file is gone.

ios::app is your friend when you need to append. ios::ate positions the cursor at the end but still lets you seek backward. ios::binary turns off newline translations that corrupt binary data on Windows.

Always be explicit. Write std::ofstream logfile("transactions.log", std::ios::app) and never rely on implicit behavior. Mode flags compose with bitwise OR — that's not a coincidence, it's a design choice that gives you fine-grained control. Use it.

FileOpenModes.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// io.thecodeforge — c-cpp tutorial

#include <fstream>
#include <iostream>

int main() {
    // Explicit append mode — safe for logs
    std::ofstream log("transactions.log", std::ios::app);
    if (!log) {
        std::cerr << "Failed to open log file for appending\n";
        return 1;
    }
    log << "Transaction committed at " << 1712345678 << "\n";
    log.close();

    // Binary read mode — no implicit truncation
    std::ifstream config("settings.bin", std::ios::in | std::ios::binary);
    if (!config) {
        std::cerr << "Config file not found — using defaults\n";
        return 1;
    }
    // ... read binary data
    config.close();
    return 0;
}
Output
Transaction committed at 1712345678
Config file not found — using defaults
Production Trap:
Never open ofstream with default mode unless you intend to erase. One silent truncation in a loop and your audit trail is gone. Always specify ios::app or ios::ate for persistent files.
Key Takeaway
Explicit file modes prevent silent data loss. Always specify ios::app for append, ios::in for read, and ios::binary for non-text data.

File Open Failure — Checking Is Cheap, Silently Corrupting Is Not

A file open failure is not an exception. It's a silent state change in the stream object. If you don't check, you'll read garbage, write to nowhere, and wonder why your report is empty.

The is_open() method or the boolean conversion operator on the stream will tell you the truth. Use one of them. Every time. Before reading or writing a single byte.

Common failure causes: file doesn't exist for ios::in, permissions insufficient for write, path is a directory, or the disk is full. These aren't edge cases — they happen in production daily.

Pattern: open, check, bail or handle. Never assume. If opening a config file fails, fall back to defaults, log the error, and continue. If opening a database file fails, stop and scream. The response depends on criticality, but the check is mandatory.

SafeFileOpen.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// io.thecodeforge — c-cpp tutorial

#include <fstream>
#include <iostream>

int main() {
    std::ifstream config("config.ini");
    if (!config.is_open()) {
        // Fallback: use hardcoded defaults
        std::cerr << "config.ini not found, using defaults\n";
        int retries = 3;
        std::cout << "Retries: " << retries << "\n";
        return 0;
    }
    // Read config
    std::string line;
    while (std::getline(config, line)) {
        std::cout << line << "\n";
    }
    config.close();
    return 0;
}
Output
config.ini not found, using defaults
Retries: 3
Senior Shortcut:
Boolean conversion (if (file)) works on all stream states, not just open. But is_open() is more precise — it only fails if the open call itself failed. Use it for open checks.
Key Takeaway
Check file open success immediately. Always. Use is_open() or the stream's boolean operator before any read or write.
● Production incidentPOST-MORTEMseverity: high

Production Log Truncation Caused by w Mode in File Rotation

Symptom
After a routine deployment restart, the monitoring dashboard shows 'No data available' for the past 2 hours. The log file daemon.log exists but contains only the header written after the restart.
Assumption
The log file would be appended or renamed on restart. The developer assumed fopen with 'w' would create a new file if it didn't exist, but never considered the case where the file already existed.
Root cause
fopen("daemon.log", "w") opens the file for writing and truncates any existing content to zero bytes. The deployment script restarted the daemon without rotating the log file, causing the entire previous log to be lost.
Fix
Change the mode from 'w' to 'a' (append) to preserve existing data, or implement log rotation before restart. Also add a check to exit if the file already exists when expected to be new.
Key lesson
  • Always verify the fopen mode matches the intended behaviour — 'w' is destructive.
  • Implement log rotation or appending as a deployment step to avoid data loss.
  • Add a startup check that logs a warning if the file already exists when opening in 'w' mode.
Production debug guideQuick symptom-to-action mapping for common file handling failures4 entries
Symptom · 01
fopen returns NULL despite file existing
Fix
Check file permissions (stat command on Linux), verify file path is absolute/relative correctly, check disk space with df -h, and use perror() to print the system error.
Symptom · 02
fread returns fewer items than requested
Fix
Check ferror() to distinguish between EOF and error. If ferror is 0, it's EOF – partial read. If ferror is non-zero, I/O error occurred. Use perror() to see the error.
Symptom · 03
Written data never appears in file until program exits
Fix
Add fflush() after each write if immediate persistence is required, or ensure fclose() is called. Check if buffer size is large (setvbuf can help).
Symptom · 04
Binary file read shows corrupted data on different machine
Fix
Check endianness and struct packing. Use fixed-size types (int32_t, etc.) and pack the struct with __attribute__((packed)) or #pragma pack(1).
★ C File I/O Quick Debug Cheat SheetWhen file operations fail, here's the exact commands and fixes to diagnose the problem.
File not found despite correct path
Immediate action
Check current working directory with getcwd() or print realpath("app_log.txt")
Commands
ls -la app_log.txt (check file existence and permissions)
perror("fopen") — prints the system error message
Fix now
Use absolute path or ensure the file is in the directory where the process runs.
Last write never reaches disk+
Immediate action
Call fflush(your_file) immediately after the write
Commands
Check fclose() return value – non-zero means flush failed
strace -e trace=write,close ./your_program (trace system calls)
Fix now
Ensure every exit path calls fclose(). Use a cleanup macro or goto cleanup pattern.
fscanf fails to parse structured text+
Immediate action
Use fgets() instead to read whole lines, then parse with sscanf or strtok
Commands
Print the raw line with printf("Raw line: '%s'", buffer) before parsing
Check for mismatched format specifiers (e.g., %s expects char*, not int*)
Fix now
Replace fscanf with fgets + careful tokenisation.
Binary file size doesn't match expected number of records+
Immediate action
Check sizeof of struct using printf("%zu", sizeof(Employee))
Commands
ls -l employees.dat; stat employees.dat
xxd employees.dat | head -20 (hex dump to inspect raw bytes)
Fix now
Ensure struct is packed and use fixed-size types. Verify fwrite count matches records written.
Text Mode vs Binary Mode in C File I/O
AspectText Mode ('r', 'w', 'a')Binary Mode ('rb', 'wb', 'ab')
Newline handlingTranslates '\n' ↔ '\r\n' on WindowsNo translation — raw bytes only
Human readableYes — open in any text editorNo — garbage in a text editor
Portability across OSesSafe for text dataSafe for binary data; byte order varies by CPU
Portability across compilersFineRisk of struct padding differences between builds
Use caseConfig files, logs, CSV, INIImages, audio, structs, serialised objects
fseek reliabilityUnreliable for byte offsetsFully reliable — offset = exact byte position
Performance on large dataSlower (conversion overhead)Faster (memcpy-style direct read/write)

Key takeaways

1
FILE* is a buffered abstraction over OS file descriptors
fclose() isn't optional, it's the flush trigger that ensures your last writes actually hit the disk.
2
Always check fopen()'s return value for NULL and call perror() immediately
silent NULL dereferences turn a fixable I/O error into an untraceable crash.
3
Use 'rb'/'wb' for any non-text data
on Windows, text mode silently mutates bytes in your structs by translating newline sequences, which corrupts binary reads.
4
fseek + fixed-size structs = O(1) random access
this is the pattern behind flat-file databases and most embedded data storage, and it's built entirely on sizeof arithmetic.

Common mistakes to avoid

3 patterns
×

Not checking if fopen() returned NULL

Symptom
Immediate segfault when the program tries to use the NULL FILE* pointer, with a crash message like 'Segmentation fault (core dumped)' and no useful context.
Fix
Always wrap fopen() in a NULL check followed by perror() and a clean exit; treating file I/O as infallible is the most common C file-handling bug.
×

Forgetting fclose() or only calling it on the happy path

Symptom
The last few kilobytes of written data never appear in the file, or appear only after the process exits, because the C runtime buffers writes internally and flushes on close.
Fix
Use a pattern where fclose() is called in every exit path (including error exits), or structure code so a single cleanup block always runs before returning; in complex functions, consider goto cleanup: at the end.
×

Using 'w' mode when 'a' or 'r+' was intended

Symptom
The file exists and has data, but after running the program it's empty or contains only the new data; the original content is silently gone.
Fix
Before choosing a mode, ask yourself three questions: 'Does the file need to exist already?' (r), 'Do I want to preserve existing content?' (a or r+), 'Am I okay starting fresh?' (w); when in doubt, open with 'r' first to verify existence before writing.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is the difference between fread/fwrite and read/write system calls,...
Q02JUNIOR
If you open a file with fopen in 'w' mode and immediately call fclose wi...
Q03SENIOR
How would you safely update a single record in the middle of a binary fi...
Q01 of 03SENIOR

What is the difference between fread/fwrite and read/write system calls, and when would you choose one over the other?

ANSWER
fread and fwrite are part of the C standard library and operate on FILE* streams. They provide buffered I/O — the runtime reads/writes chunks internally, reducing the number of expensive system calls. read and write are OS system calls that work on raw file descriptors. Use fread/fwrite for most application-level code because they're portable and efficient. Use read/write when you need low-level control (e.g., non-blocking I/O, managing file descriptors directly, or working with sockets or pipes).
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is the difference between text mode and binary mode in C file handling?
02
Why does my file appear empty even though I wrote data to it in C?
03
Can I read and write the same file at the same time in C?
04
How do I truncate a file to a specific length without rewriting it?
N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Everything here is grounded in real deployments.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's C Basics. Mark it forged?

7 min read · try the examples if you haven't

Previous
Memory Management in C — malloc calloc free
12 / 17 · C Basics
Next
Preprocessor Directives in C