Concepts / Understanding Class Invariants and Exception Handling

Understanding Class Invariants and Exception Handling

A try block can have several catch blocks, and the exception type decides which handler runs.

  • Programming
Interactive lab

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

  1. Statements in try run until one raises an exception; the rest of try is skipped.
  2. Python checks the except clauses in order and runs the first one whose type matches.
  3. If nothing was raised, the else block runs instead.
  4. 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.

Educational simulation

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?

  • The std::length_error handler

  • The std::bad_alloc handler

  • Both handlers, one after the other

Reveal answer
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.

try block runsException raised?Try completes, nohandler runscatchstd::length_errorNon-matching handlerskippedException type?catch std::bad_alloccompletes or throwsnoyeslength_errorbad_allocbad_alloc clause skipped
After the try block runs, is an exception raised, and if so, which catch clause matches its type?

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.

cpp
Notes on the code
  • 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.
Output
handler: negative size
after try

The 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.

cpp
Notes on the code
  • 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_alloc handler 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.

test()catch block rethrowsexception in flightsame exception, rethrowncaller of test()checks for a handlerouter handlercode outside this sourcethrow;propagatesif a handler exists
When test() rethrows with throw; and nothing in test() handles the exception again, which frame receives it next?

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.

cpp
Notes on the code
  • 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.

Exception reacheshandlerCan this functionhandle it?Handle and continuethrow; propagatesShould the callerdecide?std::terminate() endsprogramyesnoyes, pass it onno, not designed to handle it
Once an exception reaches a handler in a function, which decision leads to normal continuation, to propagation, or to std::terminate()?

Mistakes When Tracing Handlers

  • Common mistake

    Assuming every catch clause runs after a throw

    What it looks like

    Reading the two-handler test() and concluding that both the length_error and bad_alloc handlers print output for a single call.

    Why it fails

    Only one catch clause receives a given exception. The clause whose type matches is the one that runs, and the others are skipped.

    Do this instead

    For 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 like

    Assuming that a line after the throwing statement still executes because it sits inside the try block.

    Why it fails

    Control leaves the try block at the throw, so the rest of the block is skipped.

    Do this instead

    Trace 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 like

    Believing that throw; inside the length_error handler starts a fresh error with no details.

    Why it fails

    A bare throw; rethrows the exception that was caught, so that it propagates further.

    Do this instead

    Treat 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

EASY

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_alloc clause prints its message and finishes normally, just like the length_error clause did.
  • Write down the output line by line before comparing it with your prediction.
MEDIUM

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.

Related concepts

More concepts starting with U