How to Fix std::cout Exceptions Triggering Crashes on std::cerr in C++
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:
std::coutfails to write (e.g., disk full), setting itsbadbit.- Because you enabled exceptions on
badbit,std::coutthrows astd::ios_base::failure. - You catch the exception and try to print an error message to
std::cerr. - Before writing to
std::cerr, the system attempts to flushstd::coutbecause they are tied. - Flushing
std::coutwhile itsbadbitis set and exceptions are enabled triggers a secondstd::ios_base::failureexception. - Since this second exception is thrown inside your
catchblock and goes unhandled, the runtime callsstd::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.