Exception Safety

By the end of this lesson you'll be able to throw and catch exceptions correctly, let RAII clean up automatically when something fails, and choose the right safety guarantee — basic, strong, or no-throw — for every function you write.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

Part of the free C++ course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

Think of a function call as a person climbing a ladder , one rung per call. A throw is shouting "abort!" and sliding back down. As you pass each rung on the way down, you must put back anything you borrowed there — that automatic clean-up is stack unwinding , and the destructors are the "put it back" step. The three guarantees describe how tidy you leave the room when you bail out: basic = nothing dropped on the floor (no leaks); strong = the room looks exactly as it did before you entered (all-or-nothing); no-throw = you promise you will never need to abort at all.

1. Throw, Try, and Catch

An exception is an error object that travels up the call stack until something handles it. You throw it to signal "I can't continue", wrap risky code in a try block, and handle the problem in a catch . Standard error types live in : std::runtime_error for problems found at run time and std::invalid_argument for bad inputs — both derive from std::exception , whose .what() method returns your message. Read this worked example, then run it.

Notice how the line after the failing call was skipped — a throw jumps straight to the matching catch . Now your turn: fill in the three blanks to write your own throw , try , and catch .

2. Stack Unwinding & Why RAII Wins

When a throw fires, C++ unwinds the stack : it walks back up through every function it was inside and destroys each local object along the way, running its destructor. This is the secret to exception safety — if your resources (memory, files, locks, connections) are owned by local objects, they are guaranteed to be released, even on the error path. That pattern is RAII (Resource Acquisition Is Initialisation): tie a resource's lifetime to an object's lifetime, and you never have to remember to clean up manually.

3. The Three Exception-Safety Guarantees

Every function offers one of three promises about what happens when it throws. The basic guarantee says: no leaks, the object stays usable, but its state may have partly changed. The strong guarantee says: all-or-nothing — if it throws, nothing changed. The no-throw guarantee says: this can never fail, so you mark it noexcept . This example shows all three on one class.

4. The Copy-and-Swap Idiom (Strong Guarantee)

The classic recipe for the strong guarantee is copy-and-swap : do all the risky work on a copy , and only when it has fully succeeded, swap the copy into place. Because std::swap on standard containers is noexcept (it just exchanges internal pointers), the commit step can't fail — so either everything works or your original is untouched. Your turn: complete the two blanks to make addAll strong.

5. noexcept and Why It Matters

noexcept is a promise to the compiler that a function never throws. Break that promise and the program calls std::terminate immediately — so only mark things that truly can't fail (moves, swaps, simple getters). The biggest payoff is performance: std::vector will only move its elements when it grows if their move constructor is noexcept ; otherwise it must copy them to preserve its own strong guarantee.

Common mistake: forgetting noexcept on a move constructor silently makes vector fall back to copying — a quiet performance killer for types with expensive copies.

Aim for the strongest guarantee you can afford. The strong guarantee costs a copy, so use it for critical operations and the basic guarantee on hot paths.

No blanks this time — just a brief and an outline. Make addItems all-or-nothing with copy-and-swap and mark total as noexcept . Run it and confirm the rejected batch leaves the cart unchanged.

Practice quiz

Why should you catch exceptions by const reference (const std::exception&)?

  • It is faster to type
  • References cannot be caught any other way
  • Catching by value copies and slices off the derived part, losing the real type and message
  • It automatically rethrows the exception

Answer: Catching by value copies and slices off the derived part, losing the real type and message. Catching the base type by value slices the object; const reference keeps the full object and avoids the copy.

What is stack unwinding?

  • Walking back up the call stack on a throw, destroying every fully-constructed local object on the way
  • Reordering the call stack for speed
  • Clearing all global variables
  • Restarting main() from the top

Answer: Walking back up the call stack on a throw, destroying every fully-constructed local object on the way. On a throw, C++ unwinds the stack and runs destructors of locals — which is exactly how RAII frees resources.

What does the BASIC exception-safety guarantee promise?

  • Nothing changes at all if the operation throws
  • The function never throws
  • The program terminates cleanly
  • No leaks and the object stays valid, but its state may have partially changed

Answer: No leaks and the object stays valid, but its state may have partially changed. Basic guarantees no leaks and a usable object, but state may have changed; strong is the all-or-nothing one.

What does the STRONG guarantee promise on a throw?

  • No leaks, but partial changes are allowed
  • All-or-nothing: the object is exactly as it was before the call
  • The function is marked noexcept
  • Resources are leaked but the object survives

Answer: All-or-nothing: the object is exactly as it was before the call. The strong guarantee is all-or-nothing: if the operation throws, the object is unchanged.

Which idiom is the usual way to provide the strong guarantee?

  • Copy-and-swap
  • try-catch-rethrow
  • Reference counting
  • Double-checked locking

Answer: Copy-and-swap. Copy-and-swap does the risky work on a copy, then commits with a noexcept swap, giving all-or-nothing behaviour.

Why is the commit step (swap) the safe point in copy-and-swap?

  • swap is the fastest operation
  • swap deletes the old data immediately
  • std::swap on standard containers is noexcept, so the commit cannot fail
  • swap always succeeds because it copies everything

Answer: std::swap on standard containers is noexcept, so the commit cannot fail. Because swap just exchanges internal pointers and is noexcept, the commit can't throw — so either it all works or the original is untouched.

Should you ever throw from a destructor?

  • Yes, it is the cleanest way to report errors
  • No — destructors are noexcept by default, so an escaping exception calls std::terminate
  • Only in release builds
  • Only if the constructor also throws

Answer: No — destructors are noexcept by default, so an escaping exception calls std::terminate. Destructors are implicitly noexcept; an escaping exception crashes via std::terminate, so handle failable cleanup inside the destructor.

Why does marking a move constructor noexcept matter for performance?

  • It makes the move run on a separate thread
  • noexcept makes the constructor inline
  • It has no real effect
  • std::vector only moves elements on regrowth if the move is noexcept; otherwise it copies them

Answer: std::vector only moves elements on regrowth if the move is noexcept; otherwise it copies them. Without noexcept, vector falls back to copying to preserve its own strong guarantee — a quiet performance killer.

What makes RAII clean up resources automatically even when an exception is thrown?

  • The garbage collector
  • Stack unwinding runs the local object's destructor as it leaves scope
  • A try block around main()
  • The noexcept keyword

Answer: Stack unwinding runs the local object's destructor as it leaves scope. When the stack unwinds, each local's destructor runs — so resources owned by local objects are released with no manual cleanup.

What happens if a function is marked noexcept but actually throws?

  • The exception is silently ignored
  • It is automatically caught in main()
  • std::terminate is called immediately
  • The compiler refuses to build it

Answer: std::terminate is called immediately. Breaking the noexcept promise calls std::terminate, so only mark things that truly cannot fail.