while (!feof(f)) Trap: The Last Line That Prints Twice2026-06-29
This function reads a config file line by line and prints each one. It's been in production for years. Then a user reports their final config entry is being applied twice, and on an empty file the program prints garbage.
#include <stdio.h>
void dump_lines(const char *path) {
FILE *f = fopen(path, "r");
if (!f) return;
char line[256];
while (!feof(f)) {
fgets(line, sizeof(line), f);
printf("> %s", line);
}
fclose(f);
}
int main(void) {
dump_lines("config.txt");
return 0;
}
If config.txt contains three lines, the output has four. If it's empty, you get one line of uninitialized stack memory. Why?
feof(f) does not predict whether the next read will succeed. It reports whether a previous read already failed because it crossed EOF. After the third fgets succeeds — reading the file's last line and consuming the trailing newline — the stream is positioned at EOF but the EOF flag is not yet set. No read has crossed the boundary yet; one has merely reached it.
So the loop condition is still false, and we iterate one more time. The fourth fgets finally hits EOF and returns NULL — but we ignore its return value. line still holds the contents of the previous read, so printf happily prints the last line a second time. On an empty file, the very first fgets fails, and we print whatever bytes happened to live in the uninitialized 256-byte buffer.
This isn't a quirk of fgets. The same trap lurks behind fscanf, fread, and fgetc when paired with !feof(f) as a loop guard. C++ commits the analogous sin with while (!stream.eof()). The naming is honest — programs that ask "have we reached the end of the file?" before the read are asking the wrong question.
Check the return value of the read itself. fgets returns NULL on EOF or error; that's your loop terminator:
char line[256];
while (fgets(line, sizeof(line), f)) {
printf("> %s", line);
}
If you need to distinguish EOF from a read error, call feof(f) or ferror(f) after the loop, on the failure path — not as the loop condition.
A subtler variant of the bug survives even after you stop using feof as a guard: fgets(line, n, f); if (feof(f)) break; process(line);. It looks cleaner, but if fgets returns NULL due to a read error rather than EOF, feof is false, the broken read goes undetected, and process gobbles the previous iteration's buffer. The only safe pattern is: branch on what the read function told you, not on what a separate flag suggests about the stream's overall mood.
The flag tells you what happened on some call. The return value tells you what happened on this one. Always trust the return value.
feof() reports that a past read hit EOF, not that the next read will — so loop on the return value of fgets/fread, never on !feof(f).
