The C++ "Hello World" Bug and Stream Exceptions

Did you know that almost every standard C++ "Hello World" program has a silent bug? If you run a program and redirect its output to a full disk (or simulate it using /dev/full on Linux), the program will often exit with a success status (0) even though writing to std::cout failed.

To handle this, developers often enable exception throwing on std::cout using:

std::cout.exceptions(std::ios_base::badbit);

However, when an exception is thrown and caught, attempting to print the error message to std::cerr inside the catch block can trigger a secondary, unhandled exception, causing the program to crash with an abort or terminate error. Let's look at why this happens and how to fix it.

The Root Cause: Stream Tying

By default, standard C++ streams are "tied" together to ensure that prompts appear before inputs and errors are synchronized. Specifically, std::cerr is tied to std::cout.

This means that whenever you write to std::cerr, the runtime automatically flushes std::cout first. The sequence of events looks like this:

  1. std::cout fails to write (e.g., disk full), setting its badbit.
  2. Because you enabled exceptions on badbit, std::cout throws a std::ios_base::failure.
  3. You catch the exception and try to print an error message to std::cerr.
  4. Before writing to std::cerr, the system attempts to flush std::cout because they are tied.
  5. Flushing std::cout while its badbit is set and exceptions are enabled triggers a second std::ios_base::failure exception.
  6. Since this second exception is thrown inside your catch block and goes unhandled, the runtime calls std::terminate(), crashing your program.

How to Fix the Issue

Solution 1: Untie std::cerr from std::cout

The cleanest and most robust solution is to break the tie between std::cerr and std::cout at the beginning of your main function. This prevents std::cerr from automatically flushing std::cout when you write to it.

#include <iostream>
#include <exception>

int main() {
    // Untie cerr from cout
    std::cerr.tie(nullptr);
    
    std::cout.exceptions(std::ios_base::badbit);
    try {
        std::cout << "Writing some output..." << std::endl;
    } 
    catch (std::ios_base::failure const& fail) {
        std::cerr << "An I/O exception occurred!\n";
        return 1;
    }
    return 0;
}

Solution 2: Disable Exceptions Inside the Catch Block

If you prefer to keep the streams tied, you must reset std::cout's exception mask or clear its error state before writing to std::cerr inside the catch block:

catch (std::ios_base::failure const& fail) {
    // Disable exceptions on cout before writing to cerr
    std::cout.exceptions(std::ios_base::goodbit);
    
    std::cerr << "An I/O exception occurred!\n";
    return 1;
}

Solution 3: Check Stream State Manually on Exit

Instead of using C++ exceptions—which can sometimes be complex to manage around streams—you can manually check the state of std::cout before your program exits. This is often the preferred, modern C++ approach for simple CLI utilities:

#include <iostream>

int main() {
    std::cout << "Writing some output..." << std::endl;
    
    // Explicitly flush and check if the stream is still healthy
    if (!std::cout.flush()) {
        std::cerr << "Write failed!\n";
        return 1;
    }
    return 0;
}

Summary

Using std::cout.exceptions() is a powerful way to catch silent write failures, but the default behavior of std::cerr being tied to std::cout can cause unexpected crashes. By untying the streams with std::cerr.tie(nullptr), you can safely report errors to the console even when standard output is completely blocked.