C++ / CONSOLE AND FILE INPUT-OUTPUT
Writing files and handling filesystem errors
Write files with ofstream, choose truncate or append, and catch failures at close or through std::filesystem error codes and exceptions.
What you will learn
- Pick std::ios::app on purpose; the default output mode truncates the file at open
- Call close() yourself and check fail() before reporting a successful save
- Create missing parent directories with fs::create_directories before opening
- Compare an error_code to std::errc enumerators instead of numbers or message text
Understanding Writing files and handling filesystem errors
An ofstream is a memory buffer with an open file behind it. The constructor decides what happens to existing content: the default mode is ios::out | ios::trunc, which empties the file the moment it opens, while ios::app keeps the old bytes and forces every write to the end. Insertion with << copies characters into the buffer and returns, so the operating system usually sees nothing until the buffer fills, you call flush, or the stream closes. That is why a stream that still tests true after a hundred insertions tells you nothing about whether a single byte reached the disk.
Output streams have two distinct failure points. Opening can fail because the parent directory is missing, the file is read-only, or the name is really a directory, and that sets failbit, so testing if (!out) right after construction catches it. Much later, when a buffer is handed to the OS and the write is refused because the disk is full or a network share disappeared, the failure surfaces as badbit. The destructor does flush, but it discards the outcome, so if you never call close() yourself that second kind of failure is invisible; close() followed by out.fail() is the check that actually means the bytes were accepted.
Streams cannot create directories, rename, delete, or measure files, so those jobs belong to <filesystem>. Almost every function there exists twice: one overload throws std::filesystem_error, which carries the error code and the paths involved, and one takes an error_code& and reports without unwinding. Use the throwing form when a failure should abort the operation and the error_code form inside code that must keep running, and compare the code against std::errc enumerators, because the raw numbers differ between platforms. Keep the difference between a false answer and a failure in mind: exists() returning false with a clear error_code means the file is genuinely absent, while a set error_code means the question could not be answered at all.
Errors reported by the operating system are not the only source of surprise. A stream that is left open holds unflushed data, so a second part of the program that reads the same path can legitimately see an empty or half-written file. Treating close() as the boundary between "in progress" and "published" removes that ambiguity.
<filesystem>
<fstream>
<iostream>
<system_error>
namespace fs = std::filesystem;
int main() {
std::cout << std::boolalpha;
// 1. This open cannot succeed: the parent directory does not exist.
std::ofstream bad("no_such_dir/report.txt");
std::cout << "missing parent dir: is_open=" << bad.is_open()
<< " fail=" << bad.fail() << '\n';
// 2. Make the directory, then write. Default mode is out|trunc.
std::error_code ec;
bool created = fs::create_directories("out_dir", ec);
std::cout << "create_directories: created=" << created
<< " error=" << static_cast<bool>(ec) << '\n';
std::ofstream out("out_dir/report.txt");
if (!out) { std::cerr << "cannot open out_dir/report.txt\n"; return 1; }
out << "alpha\n" << 42 << '\n';
out.close(); // flush + close, then judge the result
std::cout << "after close: fail=" << out.fail() << '\n';
// 3. app keeps whatever is already in the file.
std::ofstream more("out_dir/report.txt", std::ios::app);
more << "beta\n";
more.close();
std::cout << "bytes on disk: " << fs::file_size("out_dir/report.txt") << '\n';
// 4. Non-throwing query: ask the error_code what went wrong.
fs::file_size("out_dir/missing.txt", ec);
std::cout << "missing file, ENOENT="
<< (ec == std::errc::no_such_file_or_directory) << '\n';
fs::remove_all("out_dir");
}
Output is buffered, so a write only reaches the operating system at flush or close, and that is the only place a real write failure can be observed.
Worked examples
Truncate versus append
Shows that the default output mode wipes the file at open and that append mode ignores seekp.
<filesystem>
<fstream>
<iostream>
<string>
int main() {
{
std::ofstream f("log.txt"); // out | trunc
f << "first\n";
}
{
std::ofstream f("log.txt"); // truncates again
f << "second\n";
}
{
std::ofstream f("log.txt", std::ios::app); // keeps the content
f.seekp(0);
f << "third\n";
}
std::ifstream in("log.txt");
std::string line;
while (std::getline(in, line)) std::cout << "| " << line << '\n';
in.close();
std::filesystem::remove("log.txt");
}
Example explained
Line 1The second ofstream empties log.txt at open time, before any << runs, so "first" is gone without a single write.
Line 2std::ios::app opens without truncating, which is why "second" is still there in the third block.
Line 3seekp(0) has no effect under app: the position is moved to the end before every write, so "third" lands after "second" instead of overwriting it.
Line 4Each stream lives inside its own { } block so its destructor flushes and closes before the next one opens the same name.
Two ways a filesystem call reports trouble
Contrasts the error_code overload, which treats absence as an answer, with the throwing overload that carries the paths.
<filesystem>
<iostream>
<system_error>
namespace fs = std::filesystem;
int main() {
std::cout << std::boolalpha;
std::error_code ec;
bool there = fs::exists("/definitely/not/here", ec);
std::cout << "exists=" << there
<< " error=" << static_cast<bool>(ec) << '\n';
try {
fs::rename("/definitely/not/here", "/tmp/target");
} catch (const fs::filesystem_error& e) {
std::cout << "rename threw, ENOENT="
<< (e.code() == std::errc::no_such_file_or_directory) << '\n';
std::cout << "path1 = " << e.path1() << '\n';
}
}
Example explained
Line 1exists() with an error_code treats "the file is not there" as an answer, not a failure, so it returns false and leaves ec clear.
Line 2The throwing overload of rename turns the same ENOENT condition into a filesystem_error instead of a return value.
Line 3Comparing e.code() to std::errc::no_such_file_or_directory is portable, while comparing e.code().value() to 2 only works on POSIX systems.
Line 4filesystem_error remembers the paths, and the stream inserter for path prints them quoted, which is where the surrounding " " comes from.
Important notes
A successful close() only means the operating system accepted the bytes, not that they would survive a power cut; that needs fsync or FlushFileBuffers, which iostreams does not expose.
On Windows a text-mode stream turns each '\n' into two bytes, so file_size will exceed the number of characters you wrote unless the stream was opened with std::ios::binary.
Common mistakes
Opening a log as std::ofstream log("log.txt") on every run and losing every earlier line, because the default mode truncates at open; std::ios::app is what adds to a file.
Letting the destructor do the closing, so a flush that fails on a full disk sets badbit on an object nobody inspects again and the program cheerfully prints "saved" over a truncated file.
Calling fs::file_size or reopening the file for reading while the ofstream is still open, then seeing 0 bytes or a missing last line because the buffer has not been handed to the OS yet.
Try it yourself
Change, predict, then run
Write bool append_line(const std::filesystem::path& p, const std::string& text) that creates any missing parent directories, opens with std::ios::app, writes the line, closes explicitly, and returns true only if both the open and the close succeeded. Call it twice with "data/notes.txt" to prove the first line survives, then once with "data/notes.txt/oops.txt" to see it return false because a regular file cannot be a directory.
Open the C++ workspaceCheck your understanding
A program opens an ofstream, writes 50,000 lines, prints "saved", and returns from main without calling close(). Halfway through the writing, the filesystem runs out of space. What happens?
- The insertion that runs out of room throws std::ios_base::failure
- The destructor throws std::filesystem_error naming the file
- It prints "saved" and exits normally: the destructor flushes, the flush fails, and nobody ever inspects the resulting badbit
- It exits with a nonzero status, because leaving main with a stream in a bad state counts as failure
Show answer
By default a stream reports trouble by setting badbit, and here that happens inside a destructor whose result is discarded, so the failure never becomes visible. The first option is tempting because most languages raise on a failed write, but a C++ stream only throws if you asked for it with out.exceptions(std::ios::badbit), and nothing in the language turns stream state into an exit code.