C / FILES AND ERRORS
fclose, flushing and why writes can still fail
Check fclose and fflush properly so you notice failed writes, and understand why a successful fprintf does not mean your bytes reached the disk.
What you will learn
- Check fclose's return value: EOF means the final flush failed and bytes were lost.
- Read fprintf's positive return as 'copied into the stdio buffer', not 'written out'.
- Call fflush where you want write errors reported early, and check its result too.
- Use fflush plus fsync(fileno(f)) before fclose when data must survive a crash.
Understanding fclose, flushing and why writes can still fail
When you call fprintf or fwrite, the bytes normally go no further than a buffer that lives inside the FILE object. glibc sizes that buffer from the file's block size, usually 4096 bytes for a regular file, and hands it to the kernel only when it fills up, when you call fflush, when the stream is closed, or when the program returns from main. That is why fprintf's return value counts the characters it formatted into the buffer: at that moment no write() has happened, so the number says nothing about whether the filesystem would accept them.
The consequence is that write errors arrive late, delivered by whichever call happens to push the buffer out. A full filesystem, an exceeded quota, a pipe whose reader closed, or a device I/O error all surface at that flush, and for a small file the only flush is the one hidden inside fclose. fclose flushes the buffer, closes the descriptor, releases the FILE object, and returns 0 or EOF with errno set; throwing that return value away means throwing away the only report you will ever get about the last buffer-full of your data. That is the exact mechanism behind a program that exits with status 0 and leaves a truncated file behind.
It helps to picture three layers: your stdio buffer, the kernel's page cache, and the device. fclose returning 0 only proves the second handoff worked, so a power cut moments later can still lose the data; when that matters, call fflush and then fsync(fileno(f)) while the descriptor is still open, and only then fclose. Note also that fclose consumes the stream whether it succeeded or not, so a second fclose or a later fprintf through that pointer is undefined behaviour rather than a retry.
<stdio.h>
<sys/stat.h>
static long file_size(const char *path)
{
struct stat st;
if (stat(path, &st) != 0)
return -1;
return (long) st.st_size;
}
int main(void)
{
const char *path = "report.txt";
FILE *f = fopen(path, "w");
if (f == NULL) {
perror("fopen");
return 1;
}
int n = fprintf(f, "temperature=21.5\n");
printf("fprintf returned %d\n", n);
printf("size on disk after fprintf: %ld\n", file_size(path));
if (fflush(f) != 0) {
perror("fflush");
fclose(f);
return 1;
}
printf("size on disk after fflush: %ld\n", file_size(path));
if (fclose(f) == EOF) {
perror("fclose");
return 1;
}
puts("fclose reported success");
return 0;
}
Buffered output means a write failure is reported by whichever call finally hands the buffer to the kernel, and that call is usually fclose.
Worked examples
A write that only fails at fclose
Shows fprintf reporting success while the actual failure is delivered by fclose.
<stdio.h>
<string.h>
<errno.h>
int main(void)
{
/* On Linux, every write to /dev/full fails with ENOSPC. */
FILE *f = fopen("/dev/full", "w");
if (f == NULL) {
perror("fopen");
return 1;
}
int n = fprintf(f, "this line never lands anywhere\n");
printf("fprintf returned %d, error flag %s\n",
n, ferror(f) ? "set" : "clear");
errno = 0;
int rc = fclose(f);
int saved = errno;
printf("fclose returned %s: %s\n",
rc == EOF ? "EOF" : "0", strerror(saved));
return rc == EOF ? 1 : 0;
}
Example explained
Line 1fopen("/dev/full", "w") succeeds because failing is a property of writing to that device, not of opening it.
Line 2fprintf returns 31 and leaves the error flag clear: 31 bytes fit in the stdio buffer, so no write() was attempted yet.
Line 3fclose performs that write(), receives ENOSPC, and therefore returns EOF instead of 0.
Line 4errno is copied into saved before printf runs, because any later library call is allowed to overwrite errno.
Buffering decides when you find out
The same failing device, but with buffering turned off, reports the error at fprintf instead.
<stdio.h>
<string.h>
<errno.h>
int main(void)
{
FILE *f = fopen("/dev/full", "w");
if (f == NULL) {
perror("fopen");
return 1;
}
/* No buffer, so each fprintf turns straight into a write(). */
setvbuf(f, NULL, _IONBF, 0);
errno = 0;
int n = fprintf(f, "now it fails immediately\n");
int saved = errno;
printf("fprintf returned %d, error flag %s, errno = %s\n",
n, ferror(f) ? "set" : "clear", strerror(saved));
printf("fclose returned %s\n", fclose(f) == EOF ? "EOF" : "0");
return 1;
}
Example explained
Line 1setvbuf must be called before any I/O on the stream, and _IONBF removes the buffer that delayed the failure in the previous example.
Line 2fprintf now returns a negative value and sets the sticky error flag, because the write() it issued failed on the spot.
Line 3fclose returns 0 here: the buffer is empty, so the close has nothing left to fail on and does not repeat the earlier error.
Line 4Together with the previous example this shows why both per-call checks and the fclose check are needed; neither one alone catches every case.
Durable write sequence
The order of fflush, fsync and fclose when the data has to survive a crash.
<stdio.h>
<unistd.h>
int main(void)
{
FILE *f = fopen("balance.txt", "w");
if (f == NULL) {
perror("fopen");
return 1;
}
if (fprintf(f, "1200\n") < 0) {
perror("fprintf");
fclose(f);
return 1;
}
if (fflush(f) != 0) { /* stdio buffer -> kernel */
perror("fflush");
fclose(f);
return 1;
}
if (fsync(fileno(f)) != 0) { /* kernel -> device */
perror("fsync");
fclose(f);
return 1;
}
if (fclose(f) == EOF) { /* stream released */
perror("fclose");
return 1;
}
puts("balance.txt is committed");
return 0;
}
Example explained
Line 1fflush comes first because fsync only affects data the kernel already has; unflushed bytes are invisible to it.
Line 2fileno(f) is valid only while the stream is open, which is why fsync must run before fclose, not after.
Line 3fclose is still checked: it can fail on the close itself even when the flush already succeeded.
Line 4Each error path closes the stream once and returns non-zero, so the caller never sees a half-written file treated as complete.
Important notes
/dev/full is Linux-specific: opening it succeeds and every write fails with ENOSPC, which makes these failure paths reproducible without filling a real filesystem.
Returning from main or calling exit flushes open streams, but _exit, abort and a segfault do not, so buffered bytes disappear with no error reported anywhere.
Common mistakes
Writing fclose(f); with no check, so an ENOSPC on the last buffer-full is never seen and the program exits 0 leaving a silently truncated file.
Calling fflush or a second fclose on the same FILE * after fclose failed, as a retry: the object is already released, so this is undefined behaviour and often a double-free abort.
Reading a 0 from fclose as 'the bytes are on disk', when they may sit only in the kernel's page cache and disappear in a power failure because fsync was never called.
Try it yourself
Change, predict, then run
Open /dev/full for writing and push 5000 bytes through it with fputc in a loop, printing the iteration number the first time ferror(f) is set. Then print whether fclose returns 0 or EOF, and explain why the error appeared where it did rather than at the end.
Open the C workspaceCheck your understanding
A program opens a regular file with fopen(path, "w") on a filesystem with no free space, writes 200 bytes with fprintf (which returns 200), then calls fclose. Where is the ENOSPC error delivered?
- At fclose, because its flush performs the first and only write() of those 200 buffered bytes
- At fprintf, because its return value is the number of bytes the kernel accepted
- Nowhere, because stdio retries the write internally until it succeeds
- At fopen, because opening for writing reserves space for the file up front
Show answer
The 200 bytes fit in the stdio buffer, so no system call happened during fprintf; the flush inside fclose issues the write, gets ENOSPC, and returns EOF with errno set. Option 1 is tempting because the count matches the bytes exactly, but that number describes formatting into the buffer, not delivery to the filesystem.