Understanding Class Invariants and Exception Handling
A try block can have several catch blocks, and the exception type decides which handler runs.
Try it: try / except / else / finally
How an exception jumps out of a try block to the first matching except clause, when else runs, and why finally always runs.
How it works
- Statements in try run until one raises an exception; the rest of try is skipped.
- Python checks the except clauses in order and runs the first one whose type matches.
- If nothing was raised, the else block runs instead.
- finally runs in every case.
Default run (9 steps): Call safe_divide('4'). … Return 25.0. Output was: Worked: 25.0 / Done.
Simplified: One fixed function; int() and / are modelled exactly (including how Python prints floats), but nothing is executed as Python.
Loading the simulation…
Predicting Which Handler Runs
Exception handling is easiest to learn by reading code that uses it. Picture a function that wraps a single operation in a try block and attaches two separate handlers to that block. One handler accepts std::length_error, which describes a negative size, and the other accepts std::bad_alloc, which describes memory exhaustion. Before reading further, try to predict which handler runs for each kind of failure.
What do you think happens?
If the try block throws a std::bad_alloc, which catch block receives it?
Reveal answer
The std::bad_alloc handler. The length_error handler is skipped.
Each catch clause names the type of exception it accepts. The type of the thrown exception decides which clause matches, and only the matching handler runs.
Keep one question in mind as you read the rest of this article: at the moment an exception is raised, which handler does control reach, and what happens to the rest of the try block? Answering that question precisely is the skill this article builds. Each example below uses the same idea.
Tracing the Two-Handler Function
A try block can be followed by several catch blocks, and each one names the type of exception it handles. This is how one function can respond differently to different kinds of failure. The diagram below shows the decision logic in the order the program applies it. If no exception is raised, the try block completes and no handler runs. If an exception is raised, the type of that exception decides which catch block receives it. The matching handler runs, and the other handler is skipped.
Now trace the function step by step. Assume the exception raised is the length error, which the source labels as the negative size case. Control enters the try block and reaches the statement that raises the exception. Execution of the rest of the try block stops at that point. Control then checks the catch clauses in order, finds the one that names std::length_error, and runs that handler. The bad_alloc handler is never entered. Being able to say which handler matched and what happened next is the skill that lets you find where a result diverged from what you expected.
How the Exception Type Selects a Handler
The rule that governs selection is simple. The thrown exception has a type, and the catch clause that names a matching type is the one that receives it. Statements in the try block that come after the throw never run, because the throw transfers control out of the try block. This is the point where execution diverges from the straight-line path a reader might expect. The handler that receives control is chosen by type, not by the order in which you wrote the statements inside the try block.
The example below is an illustrative version written for this article. It follows the structure the source describes, with a try block and two catch clauses named for std::length_error and std::bad_alloc, but it fills the handler bodies with output so that the path is visible. The source's own version leaves the bodies as comments.
- The try block holds the operation that might fail.
- Each catch clause names the single exception type it accepts.
- The statement after the try-catch construct runs whenever a handler finishes normally.
handler: negative size
after tryThe output above follows from the rules. Calling test with a negative value raises a length error, the length_error clause matches, and its handler prints its message. The bad_alloc clause is skipped. Because that handler finishes normally, control continues to the statement after the construct. Calling test with a very large value would instead select the bad_alloc clause and skip the other one.
Passing an Error Onward with throw;
Sometimes a function catches an exception, does something useful, and then decides the error should continue upward. The source calls this rethrowing. A bare throw statement, one with no operand, placed inside a catch block rethrows the exception that was caught, so that it propagates further. The handler does not stop the error. It performs its own work and then hands the same error onward. The source describes this in a version of test() where the length_error handler prints a message and then executes a bare throw.
- The bare throw; has no operand and rethrows the exception currently being handled.
- Because of the rethrow, the statement after the try-catch construct is never reached for this exception.
- The
bad_allochandler ends the program, as explained in a later section.
The key change is that control does not finish normally after the length_error catch block. The exception leaves the handler and continues on its way. Whether anything further up handles it depends on code outside this function, which the source does not show. The diagram below illustrates that continuation in terms of the call stack.
Custom Exception Classes as Carriers
The standard exception types only tell a handler what category of problem occurred. A custom exception class can describe the situation in more detail. The source states the purpose directly: a custom class can carry arbitrary information from the place where an error is detected to the place where that error can be handled. The class is not just a label. It is a container for whatever details the handler will need. The exception object is a vehicle for information, and its class defines what that information can look like.
The source does not show a full class definition, so the sketch below is a generated illustration of the idea rather than the source's code. It stores a line number and a message at the throw site, and a handler far away can read them back later.
- The constructor fills the object with details known at the detection point.
- The accessor functions let a handler read those details later.
- A local variable in the throwing function disappears when that function exits, but the exception object carries its contents to the handler.
Ending the Program with std::terminate()
Not every exception should be passed onward or recovered from in the current function. In the second handler of the rethrowing example, the code catches std::bad_alloc and calls std::terminate(). The reasoning in the source is that test() is not designed to handle memory exhaustion. The function has no sensible way to continue, so the code ends the program on purpose instead of pretending the error was handled. The source states that std::terminate() should be used deliberately for this reason.
Mistakes When Tracing Handlers
Common mistake Assuming every catch clause runs after a throw
What it looks likeReading the two-handler test() and concluding that both the length_error and bad_alloc handlers print output for a single call.
Why it failsOnly one catch clause receives a given exception. The clause whose type matches is the one that runs, and the others are skipped.
Do this insteadFor each exception, identify its type first, then select the single matching handler.
Common mistake Expecting statements after the throw inside the try block to run
What it looks likeAssuming that a line after the throwing statement still executes because it sits inside the try block.
Why it failsControl leaves the try block at the throw, so the rest of the block is skipped.
Do this insteadTrace from the throw point directly to the catch clause that receives it.
Common mistake Thinking a bare throw; creates a new exception
What it looks likeBelieving that throw; inside the length_error handler starts a fresh error with no details.
Why it failsA bare throw; rethrows the exception that was caught, so that it propagates further.
Do this insteadTreat throw; as passing the same error onward, not as raising a new one.
Common mistake Reaching for std::terminate() whenever an error seems serious, rather than when the function is not designed to handle it
Tracing Your Own Handler Path
Using the first generated version of test() from the selection section, work out the exact output of a call to test with the argument 2000000. Name the handler that runs and say whether the statement after the try-catch construct executes.
Hints
- Check the second if statement first, since 2000000 is larger than 1000000.
- The
bad_allocclause prints its message and finishes normally, just like thelength_errorclause did. - Write down the output line by line before comparing it with your prediction.
In the rethrowing version of test(), call test with -1 and then ask whether the line 'after try' would print. Explain where control goes after the throw; inside the length_error handler, and say what determines whether anything further up handles the error.
Hints
- Ask whether the function ends normally after the rethrow.
- The source does not show code outside
test(), so reason about what the function itself guarantees.
Key Takeaways
- A try block can be followed by several catch blocks, and the type of the thrown exception decides which single handler runs.
- Statements after the throw inside the try block are skipped, and control moves directly to the matching catch clause.
- A bare throw; inside a catch block rethrows the caught exception so that it propagates further, while std::
terminate()ends the program when a function is not designed to handle the error. - A custom exception class can carry arbitrary information from the point of detection to the point of handling.