11. Files & Error Handling¶
What & why¶
Programs that forget everything when they exit aren't very useful. This lesson covers reading and writing files, working with paths, and handling the things that go wrong — missing files, bad input, corrupt data — without crashing.
Writing a file¶
#include <fstream>
#include <iostream>
int main() {
std::ofstream out{"notes.txt"}; // opens (creates/truncates) for writing
if (!out) {
std::cerr << "could not open notes.txt\n";
return 1;
}
out << "first line\n";
out << "value: " << 42 << '\n';
} // out goes out of scope → file is flushed and closed automatically
std::ofstream— output file stream. Same<<you use withstd::cout.std::cerr— standard error; use it for error messages.- You don't need to call
close(). The stream closes itself when it goes out of scope. This pattern — resources released automatically by scope — is called RAII, and it's central to C++.
Append instead of overwrite:
Reading a file¶
Line by line (the most common):
std::ifstream in{"notes.txt"};
if (!in) {
std::cerr << "could not open notes.txt\n";
return 1;
}
std::string line;
int line_no = 0;
while (std::getline(in, line)) {
std::cout << ++line_no << ": " << line << '\n';
}
Word by word or number by number:
The whole file into a string:
#include <sstream>
std::string read_all(const std::string& path) {
std::ifstream in{path};
std::ostringstream buffer;
buffer << in.rdbuf();
return buffer.str();
}
std::filesystem¶
#include <filesystem>
namespace fs = std::filesystem;
fs::path p{"data/tasks.txt"};
p.filename(); // "tasks.txt"
p.stem(); // "tasks"
p.extension(); // ".txt"
p.parent_path(); // "data"
fs::path full = fs::path{"data"} / "archive" / "old.txt"; // joins with the right separator
fs::exists(p);
fs::file_size(p); // throws if missing
fs::create_directories("data/archive");
fs::remove("old.txt");
fs::rename("a.txt", "b.txt");
for (const auto& entry : fs::directory_iterator{"."}) {
std::cout << entry.path() << (entry.is_directory() ? "/" : "") << '\n';
}
for (const auto& entry : fs::recursive_directory_iterator{"src"}) {
if (entry.path().extension() == ".cpp") {
std::cout << entry.path() << '\n';
}
}
Exceptions¶
When something goes wrong that the current function can't handle, it throws an exception. The runtime unwinds the call stack until it finds a matching catch.
#include <iostream>
#include <stdexcept>
#include <string>
int main() {
std::string text = "12x";
try {
int n = std::stoi(text); // throws std::invalid_argument for "abc"
std::cout << n << '\n'; // prints 12 — stoi stops at 'x'!
int big = std::stoi("99999999999"); // throws std::out_of_range
std::cout << big << '\n';
} catch (const std::invalid_argument& e) {
std::cerr << "not a number: " << e.what() << '\n';
} catch (const std::out_of_range& e) {
std::cerr << "number too big: " << e.what() << '\n';
}
}
- Catch by
const&. - Order matters: put more specific types first.
catch (const std::exception& e)catches every standard exception.catch (...)catches anything — use sparingly, e.g. at the top ofmain.
Throwing your own¶
#include <stdexcept>
double safe_divide(double a, double b) {
if (b == 0.0) {
throw std::invalid_argument("division by zero");
}
return a / b;
}
Common standard exception types: std::runtime_error, std::invalid_argument, std::out_of_range, std::logic_error. You can also derive your own from std::runtime_error.
Why RAII and exceptions fit together¶
When an exception unwinds the stack, every local object's destructor runs. File streams close, vectors free memory, locks unlock. No finally needed.
Streams don't throw by default¶
std::ifstream signals failure with its state, not exceptions. Always check if (!in) after opening.
std::optional: "maybe a value"¶
Not every failure is exceptional. A lookup that finds nothing is normal. Return std::optional:
#include <charconv>
#include <optional>
#include <string_view>
std::optional<int> parse_int(std::string_view s) {
int value{};
auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), value);
if (ec != std::errc{} || ptr != s.data() + s.size()) {
return std::nullopt; // failed or trailing junk
}
return value;
}
if (auto n = parse_int("42")) {
std::cout << "got " << *n << '\n';
} else {
std::cout << "not a number\n";
}
int x = parse_int("oops").value_or(0); // default if empty
std::from_chars is the fast, non-throwing, locale-independent way to parse numbers.
When to use which¶
| Situation | Tool |
|---|---|
| "Not found" / "no value" is a normal outcome | std::optional<T> |
| Caller can't reasonably continue; error is rare | throw an exception |
| Programmer bug (should never happen) | assert(condition) from <cassert> |
| Need an error reason without exceptions | std::expected<T, E> (C++23) |
A robust main¶
int main(int argc, char* argv[]) {
try {
return run(argc, argv); // all the real work
} catch (const std::exception& e) {
std::cerr << "error: " << e.what() << '\n';
return 1;
}
}
Common mistakes¶
- Not checking that a file opened. A missing file silently reads nothing.
- Relative paths are relative to the current working directory, not the executable's location.
- Catching by value (
catch (std::exception e)) — slices derived exceptions and copies. - Using exceptions for normal control flow — e.g. to exit a loop. Use
optionalor a return value. std::stoi("12x")succeeding with 12. If trailing junk matters, usestd::from_charsand check the end pointer.- Dereferencing an empty
optionalwith*— undefined behavior. Check first or use.value()(throws). - Swallowing errors:
catch (...) {}hides bugs. At minimum, log them.
Exercises¶
- Write
line_count FILEthat prints the number of lines, words, and characters in a file (likewc). Print a helpful error if the file doesn't exist. - Write a program that appends a timestamped line to
journal.txteach time it runs, andjournal --showprints the file. - Write
std::optional<double> parse_double(std::string_view)usingstd::from_charsand read numbers from a file, skipping (and reporting) bad lines by line number. - List every
.txtfile under a directory recursively, with its size, sorted largest first. - Write
copy_file SRC DSTthat refuses to overwriteDSTunless--forceis given. Use exceptions for I/O failures and catch them inmain. - Save a
std::map<std::string, int>to a file askey=valuelines, then load it back and verify it matches.
Recap¶
std::ofstreamwrites,std::ifstreamreads; checkif (!stream)after opening. Streams close themselves (RAII).std::getlinefor lines,>>for tokens.std::filesystemfor paths, directories, and file metadata; join paths with/.throw/try/catch (const T&)for exceptional failures; destructors clean up during unwinding.std::optionalfor "maybe a value";std::from_charsfor strict number parsing.
Next: Capstone: task tracker →