CodeGym /Courses /C++ SELF /Stream states: good/eof/fail/bad

Stream states: good/eof/fail/bad

C++ SELF
Level 63, Lesson 1
Available

1. Why think about stream state when reading a file

When you start reading a file, everything seems simple: you opened a std::ifstream, wrote a loop, and processed the data. But in reality reading always has several possible endings: the file could end (and that is normal), the data could be in the “wrong format” (and that is an input error), or a serious I/O problem could occur (for example, reading from a corrupted device). Without understanding the stream state you only see the symptom: “the loop stopped.”

Important idea: a stream is not just a “hose that bytes flow through.” It is an object that after each operation remembers whether the operation succeeded. If you learn to read these “notes in the margin”, you can write code that does not pretend everything is fine when the file is actually broken, the format is wrong, or you are reading something you did not expect.

If you simplify, the goal of the lecture is: learn to distinguish three situations — “we read everything to the end”, “we failed to read because the data didn’t match”, and “we failed to read because I/O broke”.

Stream state flags: goodbit/eofbit/failbit/badbit

Streams have a set of state flags (bits) that are set after read/write operations. You don’t need to memorize them as spells — it is better to imagine the stream has a small dashboard. While everything is fine — a green lamp is on. When the end of file is reached — the “EOF” lamp. When the format didn’t match — the “FAIL” lamp. When a serious problem happened — the “BAD” lamp.

Here is a human-readable table (not trying to “fit the whole standard”):

State Method What it means semantically
“everything is OK”
good()
No “bad” flag is set
“reached the end”
eof()
The stream hit the end of the input data
“operation failed”
fail()
Failed to perform input (often: format didn’t match)
“really bad”
bad()
Serious I/O error (the stream should not be trusted further)

There is a subtlety that often trips up beginners: fail() is a broader concept than “wrong format”. For example, attempting to read a number past the end of file also results in fail(). So “EOF” and “FAIL” often come together — simply because EOF becomes known after an unsuccessful read attempt.

2. Checking reads and correct loops

if (in) and converting a stream to bool

To avoid checking “all the lamps” every time, C++ provides a convenient shorthand: a stream can be tested as a boolean. Condition if (in) means roughly “the stream is currently in a good state for reading”, and if (!in) — “something went wrong”.

It’s important to understand that the boolean check of a stream is closer in meaning to “no fail state” than to “everything is perfect”. Therefore sometimes it’s useful to explicitly ask eof() or bad(), to understand more precisely what happened.

Mini example: read one number and print whether it worked.

#include <fstream>
#include <iostream>

int main() {
    std::ifstream in("numbers.txt");

    int x = 0;
    if (in >> x) {
        std::cout << "Read x = " << x << '\n';
    } else {
        std::cout << "Cannot read x\n";
    }
}

Note the style: we don’t do in >> x; and then “remember to check”; instead we check the result of the operation. This is one of the main patterns for working with streams: “do it — and check immediately”.

If you want to see the flags explicitly (for debugging), you can print the “lamps” to the console:

#include <fstream>
#include <iostream>

int main() {
    std::ifstream in("numbers.txt");
    int x = 0;

    in >> x;

    std::cout << "good=" << in.good()
              << " eof=" << in.eof()
              << " fail=" << in.fail()
              << " bad=" << in.bad() << '\n';
}

This is useful to keep in mind as a “diagnostic flashlight”. You don’t have to print it always, but when something stops strangely — it’s a quick way to see which state the stream is in.

Read directly in the loop condition

The most common mistake when reading is building a loop around the “assumption” that reading will always succeed, and then trying to handle consequences. The correct approach is the opposite: the loop should repeat while the read succeeds. That means the read operation should be in the loop condition.

If you read numbers as tokens with >>, it looks like this:

#include <fstream>
#include <iostream>

int main() {
    std::ifstream in("numbers.txt");

    long long sum = 0;
    int x = 0;
    while (in >> x) {
        sum += x;
    }

    std::cout << "sum=" << sum << '\n'; // for example: sum=60
}

If you read line-by-line, it’s analogous:

#include <fstream>
#include <iostream>
#include <string>

int main() {
    std::ifstream in("lines.txt");

    std::string line;
    while (std::getline(in, line)) {
        std::cout << "line: " << line << '\n';
    }
}

Why is it so important to “read in the condition”? Because the stream tells you the truth at the moment of attempting the read. If you first run the loop and then read, you can easily get an extra iteration, process garbage, or “reuse” an old value.

Why while (!in.eof()) is a trap

Beginners usually start their friendship with eof() from a misunderstanding. They want to do “while not end-of-file — read”. That seems logical. But eof() becomes true only after you tried to read past the file.

So the loop while (!in.eof()) almost guarantees an extra iteration: you will enter the loop, perform an unsuccessful read, and then… possibly process non-existent data.

Classic bad example:

#include <fstream>
#include <iostream>

int main() {
    std::ifstream in("numbers.txt");

    long long sum = 0;
    int x = 0;

    while (!in.eof()) {
        in >> x;
        sum += x; // x may not be updated if the read failed
    }

    std::cout << "sum=" << sum << '\n';
}

If the last in >> x could not read a number (for example, because the file ended), the variable x typically remains unchanged. And you will add the “old value” to the sum once more. This bug looks like “why is the sum sometimes one number too big?” — and it’s exactly the case where a programmer starts to suspect ghosts in their laptop.

The correct loop is only via while (in >> x) or while (std::getline(...)), as we showed above.

4. EOF is not an error: distinguishing end-of-file from a failure

When a read loop ends, the main question remains: “did we finish because we read everything, or because something broke?”. This is where eof()/fail()/bad() should be used deliberately.

A typical pattern after token reading is: “if bad() — it’s an I/O problem; if fail() and not eof() — it’s a format error”.

#include <fstream>
#include <iostream>

int main() {
    std::ifstream in("numbers.txt");

    int x = 0;
    while (in >> x) {
        // processing
    }

    if (in.bad()) {
        std::cerr << "I/O error while reading\n";
        return 1;
    }

    if (in.fail() && !in.eof()) {
        std::cerr << "Format error: expected a number\n";
        return 1;
    }

    // we get here if reading ended normally (usually by EOF)
}

It’s important to catch the logic: EOF is often a normal end of reading, but “fail without EOF” is a sign that the data does not match the expected format. For example, you read an int, and the file contained the word "cat".

For std::getline the picture is simpler: if std::getline returns false, it might be EOF or a read problem. Often checking if (!in.eof()) is enough to understand that we stopped for a non‑natural reason.

5. What fail() means and how to recover

fail() in exercises most often means: “we expected one thing and got another”. For example, we expected a number but got a string. Or we expected three fields but the line is empty. This is not a “disk catastrophe”; it’s “the user (or file) wrote something wrong”.

Let’s show a simple scenario. Suppose the file contains numbers, but suddenly a word appears:

10 20 cat 30

Code:

#include <fstream>
#include <iostream>

int main() {
    std::ifstream in("mixed.txt");

    int x = 0;
    while (in >> x) {
        std::cout << "x=" << x << '\n'; // x=10, x=20
    }

    if (in.fail() && !in.eof()) {
        std::cout << "Stopped: not a number\n"; // Stopped: not a number
    }
}

Okay, we detected the problem. Can we “continue reading” after fail()? Sometimes yes, but it requires a decision: what exactly are you skipping to resynchronize with the stream. If you simply call clear() while the input junk remains, you can get into an infinite loop: you try to read a number again, it fails again, you call clear() again… until patience runs out.

A very common “repair” technique is to reset the state and skip the rest of the line up to '\n'. This works well when the format is “one record = one line”.

#include <fstream>
#include <iostream>
#include <limits>

int main() {
    std::ifstream in("mixed_lines.txt");

    int x = 0;
    while (true) {
        if (in >> x) {
            std::cout << "x=" << x << '\n';
            continue;
        }

        if (in.eof() || in.bad()) break;

        in.clear();
        in.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
    }
}

This is not “magic”, but discipline: if a read failed, you must either stop, or skip the problematic fragment so the next read has a chance.

6. What bad() means: when it’s better to stop

If fail() is often “data doesn’t fit”, then bad() hints that the problem is deeper: an I/O error, buffer corruption, something after which continuing usually makes no sense. In real systems this can be rare, but in educational code it’s useful to keep the rule: bad() is a reason to stop and report the error clearly.

Psychologically bad() is convenient to think of as: fail() — “I couldn’t read the number”, bad() — “I’m not sure I can operate as a stream any further”.

Therefore, in careful loaders (and normal utilities) checks usually go in this order: first bad(), then fail(), and only then “maybe it’s just EOF”.

7. A scheme: how to think about reading from a file as an algorithm

To avoid keeping everything in your head as a mess of ifs, it’s useful to imagine reading as an algorithm with branches. This especially helps when you debug “why did it stop”.

flowchart TD
    A[Start: stream is open] --> B{Was the read operation successful?}
    B -->|yes| C[Process data]
    C --> B
    B -->|no| D{"bad()?"}
    D -->|yes| E[Serious I/O error -> stop]
    D -->|no| F{"fail() && !eof()?"}
    F -->|yes| G[Format error -> report/decide what to do]
    F -->|no| H["Normal end of reading (usually EOF)"]

This scheme does not require memorizing “bits” — it only requires a habit: after a loop stops always ask “why”.

8. Example: TaskBook — loading tasks line by line

To keep the topic from being “in a vacuum”, let’s continue the hypothetical learning app TaskBook — a small console task list. Previously we had commands and in-memory data handling, and now we want to load tasks from a file. The format will be simple: one line — one task:

id;done;title

For example:

1;0;Buy milk
2;1;Read C++ book (optional)

Let’s start with the model:

#include <string>

struct Task {
    int id{};
    bool done{};
    std::string title;
};

Now we’ll make a very simple parser for one line (no overengineering). The goal is not to “perfectly parse everything”, but to show the connection to stream state: we read the line via std::getline, and record format errors separately.

#include <sstream>
#include <string>

bool parse_task_line(const std::string& line, Task& out) {
    std::stringstream ss(line);

    int id = 0;
    int done_int = 0;
    if (!(ss >> id)) return false;
    if (ss.get() != ';') return false;
    if (!(ss >> done_int)) return false;

    out.id = id;
    out.done = (done_int != 0);
    out.title = line.substr(line.find_last_of(';') + 1);
    return true;
}

Yes, the parser is not perfect here (for example, we do not check all edge cases), but that’s fine: the lecture’s goal is not parsing, but understanding read termination. The parser only serves as a “load” for line-by-line reading.

Now the loader itself. And here we apply the main idea: read in a loop while (std::getline(...)), and after the loop distinguish EOF from a read problem.

#include <fstream>
#include <string>
#include <vector>

bool load_tasks(const std::string& filename,
                std::vector<Task>& tasks,
                std::string& error) {
    std::ifstream in(filename);
    if (!in) { error = "cannot open file"; return false; }

    std::string line;
    while (std::getline(in, line)) {
        Task t;
        if (!parse_task_line(line, t)) { error = "bad record"; return false; }
        tasks.push_back(t);
    }

    if (!in.eof()) { error = "read error"; return false; }
    return true;
}

Note the meaning of if (!in.eof()) after the loop. The std::getline loop could stop because of EOF (normal), or it could stop because of an I/O error (abnormal). This check is the minimal discipline for “grown-up” code.

And a mini-main showing how this can look in a program:

#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<Task> tasks;
    std::string error;

    if (!load_tasks("tasks.txt", tasks, error)) {
        std::cerr << "Load failed: " << error << '\n';
        return 1;
    }

    std::cout << "Loaded tasks: " << tasks.size() << '\n'; // Loaded tasks: 2
}

Even in this small example it’s clear: distinguishing reasons for stopping a read is not “cosmetic”, it’s a way of not lying to yourself. Because if std::getline stopped due to an error and you interpreted it as EOF, you will get “silently corrupted data”, which is almost always worse than an explicit failure.

9. Typical mistakes when working with stream state

Mistake #1: loop while (!eof()).
It looks logical but creates an extra iteration and causes processing of “old” values, because eof() is set only after attempting to read past the end of file. This is one of those bugs where the program almost always works until one day it reports a sum 10 higher, and you spend half an hour blaming math.

Mistake #2: “the loop ended — so everything is OK”.
A beginner often treats the end of while (in >> x) as a natural finish. But the loop ends equally on EOF, on “found a word instead of a number”, and on an I/O problem. If you don’t distinguish bad() and fail() && !eof() after the loop, your program will silently accept garbage as normal.

Mistake #3: after fail() they call clear() but don’t remove the cause of fail.
Clearing the flag is not enough. If the same character/token that caused the error remains in the input, the next read will fail again. This easily turns into an infinite loop “read → fail → clear → read → fail…”. After clear() you almost always need to either skip the problematic part of input or stop processing.

Mistake #4: confusing “data error” and “I/O error”.
fail() often means a format problem (data doesn’t meet expectations), while bad() means an I/O problem. These are different classes of errors. In the first case you typically report “incorrect file format”, in the second — “file read error”. If you lump them into one “something went wrong”, you make diagnostics harder for both yourself and the user.

Mistake #5: not checking !eof() after a std::getline loop.
When reading line-by-line it’s easy to forget that std::getline can stop not only by EOF. If you don’t check !in.eof(), you risk treating an “interrupted input” as a normal end of file — especially annoying if the file was being read from a network drive or an unstable environment.

1
Task
C++ SELF, level 63, lesson 1
Locked
Flags panel
Flags panel
1
Task
C++ SELF, level 63, lesson 1
Locked
Sum and status
Sum and status
1
Task
C++ SELF, level 63, lesson 1
Locked
Line parsing
Line parsing
1
Task
C++ SELF, level 63, lesson 1
Locked
Skipping garbage
Skipping garbage
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION