In our last lesson, we mastered the RAII idiom, which leverages object destructors to guarantee that resources are released, even when exceptions are thrown. This provided our first major tool for writing exception-safe code by preventing resource leaks. Now, we'll broaden our understanding of exception safety beyond just resource management.
When a function that modifies an object throws an exception, what can we say about the state of that object afterward? Is it corrupted? Is it unchanged? The answer to this question is at the heart of robust C++ design. Today, you will learn to describe the three fundamental levels of exception safety guarantees—basic, strong, and no-throw—which form a contract between a function and its caller about how it behaves in the face of errors. Mastering these concepts is a hallmark of a senior C++ developer who can build resilient and predictable systems.
The Promises a Function Can Make
When an operation fails, the state it leaves behind is critical. C++ developers have formalized a hierarchy of "promises" or "guarantees" that a function can offer about the state of objects if it throws an exception.

Let's explore these guarantees, starting from the weakest and moving to the strongest. The following article provides a concise and clear overview of each level.
This article by Arne Mertz introduces the concept of exception safety and systematically walks through the four levels of guarantees a piece of code can provide.
Please read through the article, paying close attention to the definition and implication of each guarantee: Start with the short introduction, "The four levels". Read the section explaining the no guarantee and the basic guarantee. Notice the example for "no guarantee" and how it can leak resources despite using unique_ptr. Understand that the basic guarantee is the minimum standard for usable code. Next, study the definition of the strong guarantee. Focus on its "commit-or-rollback" nature. The copy-and-swap example here is a preview of our next lesson. Finally, read about the nothrow guarantee. Pay special attention to the list of operations that must not throw, especially destructors.
Deconstructing the Guarantees
The article provides a solid framework. Let's reinforce these concepts, as they are a frequent topic in senior C++ interviews.
1. The Basic Guarantee: "No leaks, invariants hold."
This is the bedrock of exception safety. If a function provides the basic guarantee, it promises two things upon throwing an exception:
- No resources are leaked. Our previous lesson on RAII is the primary tool to achieve this.
- Class invariants are preserved. An invariant is a rule about the internal state of an object that must always be true for it to be considered valid (e.g., a
std::vector's_sizemember must never be greater than its_capacity).
After an exception, the object is in a valid state. However, its specific value is likely different from what it was before the call. The object is safely destructible and can be assigned a new value, but you cannot assume anything about its old state. For many functions, this is a perfectly acceptable and efficient guarantee to provide.
2. The Strong Guarantee: "All or nothing."
This guarantee provides transactional semantics. It promises that if the function terminates by throwing an exception, the state of the program is exactly as it was before the function was called.
- The operation effectively becomes atomic: it either completes successfully, or it has no observable effect.
- This is the easiest guarantee for a caller to reason about. If an error occurs, you know exactly where you stand—right back where you started.
Providing the strong guarantee often involves performing all potentially throwing work on a temporary copy of the data, and then committing the changes to the actual object using only non-throwing operations. This "copy-and-swap" pattern is something we'll implement in detail in our next lesson.
Many operations in the standard library, like std::vector::push_back when there is sufficient capacity, provide the strong guarantee "naturally"—it doesn't cost extra to implement it this way.
3. The No-Throw Guarantee: "I will never throw."
This is the strongest promise of all. A function with this guarantee will not propagate an exception to its caller. This is typically marked with the noexcept specifier in modern C++.
While it seems simple, this guarantee is absolutely critical for the entire exception safety model to work. Certain operations must not throw. The following short video explains why with a focus on exception safety.
CppCon 2019: Ben Saks “Back to Basics: Exception Handling and Exception Safety”
This CppCon talk by Ben Saks, "Back to Basics: Exception Handling and Exception Safety," clearly defines the three guarantees and explains the critical role of no-throw functions.
First, watch the clear definitions of the three guarantees. This will reinforce what you just read. Next, watch the crucial explanation of why destructors, in particular, must not throw. The key takeaway is that if an exception is already active (during stack unwinding) and a destructor throws a second exception, the program immediately calls std::terminate.
Because of this rule, the following operations are expected to be noexcept:
- Destructors: To allow for safe stack unwinding.
operator delete: Often called from destructors.- Swap functions: These are the cornerstone of the "commit" phase for many strong-guarantee implementations.
- Move constructors and move assignment operators: While not a strict language requirement, for them to be used effectively by standard library containers (e.g., to allow a
std::vectorto reallocate without making copies), they should benoexcept.
A Senior Developer's Perspective
As you write and review code, it's important to think about which guarantee each function provides. A pragmatic approach, as explained by veteran C++ speaker Jon Kalb, is to assume the basic guarantee by default and only provide stronger guarantees when they are needed and don't add undue performance overhead.
CppCon 2014: Jon Kalb "Exception-Safe Code, Part I"
In this classic CppCon talk, Jon Kalb discusses the practical implications of thinking in terms of these guarantees.
Watch the segment from the caller's point of view. Pay attention to these key points for a senior engineer: As an implementer, your code must meet the basic guarantee. As a caller, you should assume a function only meets the basic guarantee unless it is documented to be stronger. You should only provide the strong guarantee when it's "natural" (doesn't add significant cost) or when the user explicitly requires it. Document it when you do.
Conclusion
You now have a solid theoretical framework for reasoning about how functions behave in the presence of exceptions. This is a far more nuanced and powerful approach than simply wrapping code in try/catch blocks.
Here are the key takeaways:
- Exception Safety Levels: Functions can offer different levels of guarantees about object state when they throw: No Guarantee (unacceptable), Basic, Strong, and No-Throw.
- The Basic Guarantee: This is the minimum standard for robust code. It ensures no resource leaks and that object invariants are maintained, leaving the object in a valid but potentially changed state. RAII is a key tool here.
- The Strong Guarantee: This offers transactional, "commit-or-rollback" semantics. If the operation fails, the program state is unchanged, making it easy for callers to reason about.
- The No-Throw Guarantee: This is an absolute requirement for fundamental operations like destructors and swap functions to enable the entire system of exception safety.
In this lesson, we've focused on the "what" and "why" of these guarantees. In our next lesson, we will focus on the "how" by implementing the strong exception safety guarantee using the powerful and idiomatic copy-and-swap idiom.