try, throw, catch

A throw unwinds the stack, destroying every local object along the way (running their destructors), until it finds a matching catch. You can throw any type, but in practice always throw something derived from std::exception — it gives callers a uniform .what() to read, and lets a single catch (const std::exception &) handle every well-behaved exception in the codebase.

#include <stdexcept>

double divide(double a, double b) {
  if (b == 0.0) throw std::invalid_argument("divide by zero");
  return a / b;
}

try {
  divide(1.0, 0.0);
} catch (const std::invalid_argument &e) {
  std::cout << "caught: " << e.what() << '\n';
} catch (const std::exception &e) {
  std::cout << "some other standard exception: " << e.what() << '\n';
} catch (...) {
  std::cout << "caught something not derived from std::exception\n";
}
            
Catch clauses are tried top to bottom, and the first match wins — so order them most-specific to least-specific, ending with catch (...) as a last resort if you want one. Catching by const& (not by value) avoids slicing a derived exception type down to its base.

custom exception types


class ParseError : public std::runtime_error {
public:
  explicit ParseError(const std::string &msg) : std::runtime_error(msg) {}
};

throw ParseError("unexpected token at line 12");
// caught either as ParseError specifically, or generically as std::exception
            
Thrown from deep inside a call chain and caught two different ways, with a ScopeTracer local object in each frame to make stack unwinding visible: TopNotchNote/cpp/exceptions_custom_parseerror_demo.cpp
One thing worth retiring if you learned it from older material: dynamic exception specifications (void f() throw(int); listing exactly which types a function may throw) were deprecated in C++11 and removed entirely in C++17 — the compiler no longer enforces them and some compilers reject the syntax outright. The one surviving, actively-used form is the unparameterized noexcept below.

noexcept

noexcept is both a specifier and an operator, and they answer different questions. As a specifier on a function, it's a promise that the function won't let an exception escape — if one does anyway, std::terminate is called immediately rather than unwinding normally. As an operator, noexcept(expr) is a compile-time check that returns true if expr is declared not to throw.

void logMessage(const std::string &s) noexcept {   // promise: this never throws
  std::cout << s << '\n';
}

static_assert(noexcept(logMessage("x")), "expected logMessage to be noexcept");
            
Move constructors and move assignment operators are the case that matters most in practice: mark them noexcept whenever they genuinely can't throw. Containers like std::vector check this at compile time — if a type's move constructor isn't noexcept, growing the vector falls back to copying elements during reallocation instead of moving them, specifically so that a mid-reallocation exception can't leave the vector in a corrupted, half-moved state.

reading and writing files

<fstream> gives you ifstream (input), ofstream (output), and fstream (both) — each is RAII: the file closes automatically when the stream object goes out of scope, so an explicit .close() is rarely necessary, just good documentation of intent.

std::ofstream out("results.txt");
out << "index,value\n";
out << 1 << ',' << 3.14 << '\n';
// closes automatically when out goes out of scope

std::ifstream in("results.txt");
std::string header;
std::getline(in, header);       // read one line

int index; double value; char comma;
while (in >> index >> comma >> value) {   // >> stops at whitespace, so ',' needs its own read
  std::cout << index << " -> " << value << '\n';
}
            
By default, a failed file open (bad path, no permissions) doesn't throw — the stream just silently enters a failed state, and every subsequent read returns nothing. Check explicitly, or opt into exceptions:

std::ifstream in("maybe_missing.txt");
if (!in) {                                     // check 1: did the open succeed?
  std::cerr << "failed to open file\n";
}

std::ifstream strict;
strict.exceptions(std::ifstream::failbit | std::ifstream::badbit);  // check 2: opt into throwing instead
try {
  strict.open("maybe_missing.txt");
} catch (const std::ios_base::failure &e) {
  std::cerr << "open failed: " << e.what() << '\n';
}
            

RAII: the pattern underneath all of this

File streams closing themselves, unique_ptr deleting itself, a mutex unlocking itself when a lock_guard goes out of scope — all the same idea: Resource Acquisition Is Initialization. Tie a resource's lifetime to a stack object's constructor/destructor, and the resource is released correctly on every exit path — a normal return, an early return, or an exception unwinding the stack — without a single explicit cleanup call. It's the reason exceptions and manual resource management coexist safely in C++: an exception unwinding the stack still runs every local object's destructor on the way out.

where to go from here

Smart Pointers — RAII applied to heap memory specifically.
Classes & Constructors — writing your own RAII wrapper class.
C++ Best Practices — RAII as a named idiom, alongside the rest of the modern-C++ checklist.

reference

cppreference — std::exception
cppreference — std::fstream
cppreference — noexcept specifier