RAII (and What It Doesn’t Solve)
Resource Acquisition Is Initialization is the worst-named good idea in programming. Bjarne Stroustrup coined the phrase in the early 1990s, and almost everyone who hears it for the first time concludes the name means something other than what it means. The name describes the mechanism (resources are acquired in constructors and released in destructors), not the purpose (deterministic cleanup at scope exit, including exception-induced scope exit).
A better name, retroactively, would be Scope-Bound Resource Management — SBRM, occasionally seen in C++ literature. But the field stuck with RAII, so we will too.
This chapter does three things. First, restate RAII precisely enough to argue about. Second, walk through the canonical applications. Third — and this is the chapter’s actual reason for existing — enumerate what RAII does not solve, because the working assumption that “we have RAII, so we’re exception-safe” is one of the most common and most incorrect beliefs in production C++.
RAII, precisely
The mechanism is: an object’s constructor acquires a resource, and its destructor releases it. The destructor will run at scope exit, regardless of how scope is exited (return, fall-through, throw). Therefore, if you wrap a resource in such an object and only manipulate it through that object, the resource will be released exactly once on every control-flow path, including the exception path.
Three properties matter:
-
Determinism. The destructor runs at a precisely known point — when the object’s lifetime ends. In a stack-allocated case, that’s scope exit. There is no garbage collector latency, no finalizer queue, no maybe-eventually. This is what distinguishes RAII from
try { } finally { }-style cleanup in garbage-collected languages: the cleanup is structural, not procedural. -
Exception integration. The unwinder runs destructors as it walks up the stack. This is the mechanism by which RAII is exception-safe: the destructor doesn’t need to know about exceptions, and the throw site doesn’t need to know about the destructor. They are connected only by being in the same scope, which is exactly the connection the language already has machinery for.
-
Composition. RAII objects compose: an RAII object that owns another RAII object cleans up in the right order automatically, because destruction runs in reverse declaration order. You can build arbitrarily complex resource graphs and the cleanup is mechanical.
C++ leans hard on this. The Standard Library provides std::unique_ptr (single owner), std::shared_ptr (refcounted), std::lock_guard and std::unique_lock (mutex acquisition), std::ifstream/std::ofstream (files), std::thread (joining or detaching on destruction), std::vector (memory), std::scoped_lock (multi-mutex, deadlock-free acquisition order), and many more. Almost everything in modern C++ that owns a resource is an RAII type.
Where RAII works
The pattern is at its best when:
- The resource has a single owner at any time.
- The resource’s release is itself no-throw (or, at worst, can be safely ignored on failure).
- The release operation is idempotent or reliably called exactly once.
Memory, file descriptors, mutexes, scoped database transactions, scoped logging contexts, scoped feature flags — all are good fits. Here’s a scoped-mutex example to fix the pattern visually:
class BankAccount {
std::mutex mu_;
int balance_;
public:
void deposit(int amount) {
std::lock_guard<std::mutex> lock(mu_);
balance_ += amount;
} // lock released here, including via exception
};
If balance_ += amount throws (it can’t, but if it could) the lock is released. If we returned normally, the lock is released. There is exactly one cleanup site, which is the destructor of lock_guard, which the language calls automatically.
A more interesting example: scope guards. Andrei Alexandrescu’s ScopeGuard (and Boost’s BOOST_SCOPE_EXIT, and C++23’s P0052) generalizes RAII to arbitrary cleanup actions:
void update_two_files() {
write_to_file_A();
auto rollback_A = make_scope_guard([] { restore_file_A(); });
write_to_file_B();
auto rollback_B = make_scope_guard([] { restore_file_B(); });
// both succeeded; commit by dismissing the guards
rollback_A.dismiss();
rollback_B.dismiss();
}
If write_to_file_B() throws, rollback_A runs in its destructor and undoes the change to file A. (rollback_B was never constructed, since the throw happened before the assignment.) If both succeed, both guards are dismissed and neither rollback runs. This is, structurally, a transaction implemented with RAII. We’ll see more of this pattern in chapter 4.
Where RAII leaks
Now the harder part. RAII, despite the surrounding evangelism, does not solve all of exception safety. Specifically:
1. RAII solves resource problems, not invariant problems.
Recall Account::transfer_to from chapter 1:
void Account::transfer_to(Account& other, int amount) {
balance_ -= amount;
log_transfer(amount, &other); // throws
other.balance_ += amount;
}
There is no resource leak here. There are no constructors-and-destructors that could help. The bug is that the invariant “money is conserved” is violated by a partial mutation. RAII has nothing to say about this. You can wrap every object in unique_ptr and add lock_guards on every mutex and the bug remains, because the bug is not a leaked resource.
The fix is not RAII; it is ordering — do the throwing work first, the no-throw mutations last — or scope guards, or copy-and-swap, all of which are RAII-adjacent but not RAII per se. The destructor mechanism is doing structural work for you, but the work it’s doing is “run this rollback”, which you had to write yourself.
2. RAII does not protect partial construction across multiple sub-objects.
class Connection {
Socket sock_; // (a)
Buffer buf_; // (b)
std::vector<int> q_; // (c)
public:
Connection() : sock_(open_socket()), buf_(allocate_buffer()), q_() {}
};
If open_socket() succeeds (so sock_ is constructed) and then allocate_buffer() throws, what happens? The language unwinds: sock_’s destructor runs, buf_ was never constructed (throw happened in its initialization), q_ was never constructed. So sock_ is cleaned up. Good.
But consider:
class Connection {
int fd_ = -1;
char* buf_ = nullptr;
public:
Connection() {
fd_ = open_socket_raw(); // (a)
buf_ = allocate_buffer_raw(); // (b) throws
}
~Connection() {
if (fd_ != -1) close_socket_raw(fd_);
if (buf_) free_buffer_raw(buf_);
}
};
This also looks fine — the destructor cleans up, right? Wrong. The destructor of an object only runs if its constructor completed successfully. If the constructor throws, the language considers the object never to have existed; only fully-constructed sub-objects (data members) get destructed. So fd_ leaks: we set it, but our ~Connection never runs.
The fix is to wrap each raw resource in its own RAII type:
class Connection {
FileDescriptor fd_; // RAII wrapper
OwnedBuffer buf_; // RAII wrapper
};
Now if OwnedBuffer’s construction throws, FileDescriptor’s destructor runs because it is a fully-constructed sub-object. This is what the C++ Core Guidelines mean when they say “use RAII consistently”: one resource, one RAII type. Mixing raw and managed in the same class is a common mistake.
3. RAII does not save you from bad destructors.
A throwing destructor during stack unwinding from another exception calls std::terminate. This is not a theoretical issue; it shows up in real code, particularly when the destructor logs to a remote service that can fail, or commits a transaction that can fail.
class FileTransaction {
std::ofstream f_;
public:
~FileTransaction() {
f_.close();
commit_metadata(); // can throw
}
};
If FileTransaction’s scope is being unwound due to another exception, and commit_metadata() throws, the program terminates. The fix is either to absorb the exception in the destructor (and log/swallow), or to expose an explicit commit() method that can throw, leaving the destructor as a rollback.
class FileTransaction {
std::ofstream f_;
bool committed_ = false;
public:
void commit() { // explicit, can throw
f_.close();
commit_metadata();
committed_ = true;
}
~FileTransaction() {
if (!committed_) {
try { rollback(); } catch (...) {}
}
}
};
This pattern — explicit commit, automatic rollback — is the right shape for any RAII type that performs a non-trivial finalization. Boost.ScopeGuard’s dismiss() is the same idea.
4. RAII does not help across object boundaries when ownership is shared.
std::shared_ptr releases when the last reference dies. If two shared_ptrs reference each other (a cycle), neither dies. Memory leaks. This is not, strictly, an exception-safety problem — but the cycle often forms during exceptional code paths, when the cleanup that would have broken the cycle didn’t run because the throw happened first.
The standard answer is std::weak_ptr for the back-edge, with a discipline of identifying which direction “owns” the relationship and using weak_ptr for the other. In practice this discipline is honored unevenly, and shared-ownership cycle leaks are a perennial bug in production C++.
5. RAII does not address logical atomicity.
Suppose you have three updates that must be applied as a unit: write to disk, update an in-memory index, send a network message. RAII can ensure each individual resource is cleaned up. It cannot ensure the meaning of “all three or none.” If the disk write succeeds and the network send throws, you have a written-but-unreplicated state on disk. A scope guard could roll back the disk write — but rollback is itself fallible (what if the disk has filled up since?), and now you’re writing the rollback’s rollback.
This is the same problem databases solved with write-ahead logging and two-phase commit. Scope guards approximate it for the in-process case. Distributed systems use sagas. Smart contracts use the checks-effects-interactions pattern. We will see all of these in later chapters; for now, register the point that RAII handles cleanup of known resources, and leaves cross-cutting atomicity as an exercise.
6. RAII does not exist in most languages.
Java, Python, JavaScript, Go, Ruby — none of them have destructors that run deterministically at scope exit. They have approximations: Java’s try-with-resources, Python’s with, C#’s using, JavaScript’s using declarations (recently), Go’s defer. Each of these is a procedural form of RAII: instead of “construct an object, register cleanup,” it’s “explicitly say, at this scope, run this cleanup at exit.”
This is almost equivalent — and where it isn’t, the difference is exactly the asymmetry that bites you. Specifically:
- They don’t compose through ownership chains. If you put a
with-scoped Python object inside a list and then return the list, the__exit__runs at the list-scoping level, not when the list is later destroyed. There is no automatic propagation of “this thing owns that thing, so cleaning up this thing must clean up that thing.” - They are syntactic, so users can forget them. RAII in C++ enforces ownership at the type level: if you accept a
std::lock_guard&&, you have the lock; if you accept anint, you do not. In Python,open(path)andwith open(path) as f:look identical to a type checker.
We’ll come back to this in chapter 5. For now, accept that RAII as I’ve described it is a C++-shaped solution, and other languages either approximate it or just live with the consequences.
RAII is necessary and not sufficient
Consider this the chapter’s thesis. RAII is necessary because without it the basic guarantee for resource cleanup becomes a manual discipline maintained by every author of every function — and maintained inconsistently, as the catalog of historical CVEs demonstrates. With RAII, resource cleanup becomes a property of the type system, enforced structurally, and the basic guarantee for resource cleanup becomes free.
RAII is not sufficient because exception safety is more than resource cleanup. It is also invariant preservation, atomicity, and the maintenance of meaning across mutations that may be interrupted. RAII cannot help with these unless you do the work to encode them as resources. Sometimes that’s natural (scope guards, lock guards). Sometimes it isn’t (multi-object updates, cross-system invariants).
The next chapter is about the patterns that take you the rest of the way: from “no leak” to “no observable damage,” which is the strong guarantee.
Further reading
- Bjarne Stroustrup, “Why doesn’t C++ provide a ‘finally’ construct?” — http://www.stroustrup.com/bs_faq2.html#finally. Stroustrup’s argument that RAII obviates
finally. He is right, conditionally on having destructors run deterministically. - C++ Core Guidelines, §R: Resource Management — https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#S-resource.
- Eric Niebler, “C++ Coroutines: Under the covers”, on how RAII interacts with coroutines (and where it doesn’t): coroutines suspend mid-function, which is exactly the kind of partial-state-with-resources-held situation RAII was designed to prevent. Worth reading after chapter 7.