Senior 5 min · March 06, 2026

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

A daemon restart with fopen("daemon.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
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
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.

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.
● 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?
🔥

That's C Basics. Mark it forged?

5 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