Exception Handling Basics
If an exception is raised, its type decides which catch block receives it.
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…
A Lookup That Asks for a Position That Does Not Exist
Imagine writing a function that fetches the item stored at a given position in a collection. The caller passes in a position, and the function reads whatever item sits there. Most of the time the caller supplies a sensible number. But suppose the caller asks for position 10 in a collection that holds only three items. Reading that position would mean reading memory that does not belong to the collection, which can produce a wrong answer or a crash. The function has to make a decision about this case, and the decision is what exception handling is for.
What do you think happens?
A collection holds three items, and the caller asks the lookup function for position 10. What should the function do?
Reveal answer
The function should refuse the request and signal the problem with an exception instead of reading the position.
Reading position 10 would read memory outside the collection. The function instead checks its argument, and when the check fails it signals an exception so that the invalid access is visible as a distinct event rather than a quietly wrong value.
Preconditions as an Agreement Between Caller and Function
Think of a function as an agreement between the code that calls it and the code that implements it. The implementer promises a result, but only when the caller keeps up their end. The conditions the caller must satisfy before the function can do its job are the function's preconditions. Each precondition is a statement the function takes for granted on entry, and the function's work rests on those statements being true.
A precondition is an assumption a function relies on before it can work correctly. A function should consider what its preconditions are, and where feasible, test them.
A precondition is not automatically a bug in the caller's code. It is a boundary that tells the caller which inputs are acceptable. When a precondition is violated, the failure belongs to the caller's request, and the function is right to refuse the request rather than guess at what was meant. For the position lookup, the precondition is that the position must be a valid position in the collection. Stated in the form the source uses, the caller must supply a position that lies in the range from zero up to the size of the collection.
Reading the Half-Open Range
Preconditions about positions are usually written as ranges, and a convenient form is the half-open range $[a, b)$. The square bracket on the left means the start value $a$ belongs to the range. The round parenthesis on the right means the end value $b$ does not belong to the range. Every value from $a$ up to, but not including, $b$ is inside the range.
The half-open style fits positions in a collection especially well. If a collection has $n$ items, its valid positions are $[0, n)$. The lower bound $0$ is the first item, and the upper bound $n$ is excluded because no item sits there. The valid positions are therefore $0, 1, \ldots, n-1$, which is exactly $n$ positions with nothing left over.
Which values does [2, 6) include?
List every integer that belongs to the half-open range $[2, 6)$, and state how many values it contains.
- 1Identify the start
The square bracket on the left means $a = 2$ is included.
- 2Identify the end
The round parenthesis on the right means $b = 6$ is excluded.
- 3List the values
Starting at 2 and stopping before 6 gives 2, 3, 4, and 5.
- 4Count
The range holds $b - a = 4$ values.
The range $[2, 6)$ includes 2, 3, 4, and 5, and excludes 6.
- 1The position $i$ is the value the caller passes in.
- 2The left inequality $0 \leq i$ says the lower bound is included, so position zero is allowed.
- 3The right inequality $i < n$ says the upper bound $n$ is excluded, so position $n$ is not allowed.
- 4Together the statement names the precondition completely: it lists exactly which positions the caller may supply.
Testing a Precondition on Entry
A function that has identified its preconditions can check them when it runs. When the argument fails the check, the function should not carry on as if everything were fine. One clean way to report the problem is to signal it with an exception. The function stops its normal work at that point and hands control to whoever is prepared to deal with the failure. Nothing is read from an invalid location, and the caller learns that its request was rejected.
The following function implements the idea directly. It tests its precondition on entry and raises an exception when the position is out of range. This code is an original illustration written for this article, using Python's built-in exception mechanism.
b
Traceback (most recent call last):
...
IndexError: position 10 is outside the range [0, 3)- The precondition is tested on entry, before any read happens.
- The half-open check 0 <= i < n matches the range [0, n) from the notation.
- When the check fails, the raise statement stops normal work, so return items[i] never runs.
Trace the two calls. The first call passes position 1, which lies in $[0, 3)$, so the check passes and the function returns the second item, b. The second call passes position 10, which lies outside the range, so the check fails and the raise statement runs. The line that would have returned an item is never reached. This is the divergence from the normal path: the expected return is replaced by an exception.
Choosing a Handler by Exception Type
A try block can be followed by several catch blocks, and each one names the type of exception it handles. This lets one function respond differently to different kinds of failure. The sketch below shows the structure. The body of each handler is only a comment, because the point here is how try and catch blocks are arranged, not what each handler does.
- The try block holds the operation that may raise an exception.
- Each catch names one exception type it accepts.
- The handler that runs depends on which type was raised.
Read the diagram as a decision made by exception type. If the try block completes without raising anything, no handler runs. If an exception is raised, its type decides which catch block receives it. The matching handler runs and the other handler is skipped. When the raised exception is a length error, the first handler matches, and the bad_alloc handler is ignored. When the raised exception is bad_alloc, the reverse happens.
The standard exception types such as std::length_error and std::bad_alloc are not the only option. You can define your own classes and use them as exceptions. A custom exception class carries arbitrary information from the place where an error is detected to the place where it can be handled. The class is more than a label. It is a container for whatever details the handler will need, while a standard type only tells the handler what category of problem occurred. The source states this purpose but does not show a full class definition, so this article does not present one. What matters is that the exception object is a vehicle for information, and its class defines what that information can look like.
Rethrowing and Ending the Program
Sometimes a function catches an exception, does something useful, and then decides the error should continue upward. The source calls this rethrowing. Inside a catch block, a bare throw; with no operand rethrows the caught exception so that it propagates further. The handler does not stop the error. It performs its own work, such as printing a message, and then hands the same error onward. Whether anything further up handles it depends on code outside this function, which the source does not show.
- The bare throw; inside the
length_errorhandler rethrows the same exception after its own work. - The
bad_allochandler calls std::terminate()instead of continuing. - Control does not finish normally after the
length_errorcatch block.
Not every exception should be passed onward or recovered from in the current function. In the second handler of this pattern, the code catches std::bad_alloc and calls std::terminate(). The reasoning is that the function is not designed to handle memory exhaustion, so it has no sensible way to continue. std::terminate() ends the program on purpose instead of pretending the error was handled. It should be used deliberately, for exactly this kind of case.
Tracing the Rethrowing Version
Tracing means following control one step at a time and recording where it goes. Use this method on the rethrowing version of test() shown above. Each trace below assumes a different exception type is raised, and checks each step against the code.
Trace a length_error through the rethrowing handler
The try block raises std::length_error. Which handler runs, and what happens to the error afterward?
- 1Raise
Control leaves the try block at the point of the raise, so the rest of the try block is skipped.
- 2Match
The first catch names std::
length_error, so it matches. Thebad_allochandler is skipped. - 3Work
The handler prints its message for the negative size case.
- 4Rethrow
The bare throw; executes, so the same exception leaves the handler and propagates further.
The length_error handler runs, prints its message, and the exception continues upward rather than finishing normally.
Trace a bad_alloc through the same function
The try block raises std::bad_alloc. Which handler runs, and how does the program end?
- 1Raise
Control leaves the try block at the point of the raise.
- 2Match
The
length_errorhandler does not match, so it is skipped. Thebad_allochandler matches. - 3Terminate
The handler calls std::
terminate(), so the program ends.
The bad_alloc handler runs and the program ends. No rethrow occurs and nothing further up is reached.
A try block raises std::length_error inside a function whose length_error handler ends with a bare throw;. Describe the path the exception takes, then state what would change if the bad_alloc handler were the one that matched.
Hints
- Ask which catch block matches the raised type first.
- A bare throw; sends the same exception onward, so think about whether anything inside this function still runs after it.
- Compare what std::
terminate()does with what the bare throw; does.
Mistakes Beginners Make With Exceptions
Common mistake Treating a precondition violation as a result to return rather than a refusal
What it looks likeReturning a default item when the position is 10 in a three-item collection.
Why it failsThe function would be guessing at what the caller meant, which hides the misuse instead of reporting it.
Do this insteadTest the precondition on entry and signal the violation with an exception, so the failure is visible at the point it occurs.
Common mistake Reading the upper bound of a half-open range as included
What it looks likeBelieving that [0, 3) contains 3 because the range goes up to 3.
Why it failsThe round parenthesis on the right means the end value is excluded, so position 3 is not a valid position for a collection of size 3.
Do this insteadRemember that $[a, b)$ contains every value from $a$ up to but not including $b$.
Common mistake Assuming every handler will match the exception
What it looks likeExpecting the bad_alloc handler to run when the try block raised std::length_error.
Why it failsEach catch names a single exception type, and only the handler whose type matches receives the exception.
Do this insteadCheck the raised type against each catch in order and identify the one that matches; the others are skipped.
Common mistake Confusing a bare throw; with std::terminate()
What it looks likeCalling std::terminate() in a handler that should have rethrown the error so an outer function could deal with it.
Why it failsstd::
terminate()ends the program, so the error is never passed on. A bare throw; passes the same error further.Do this insteadUse throw; when the error should continue upward, and reserve std::terminate() for cases where the function cannot handle the exception and should not continue.
Common mistake Expecting code after a throw to run, when the throw has already transferred control away from that point
Practice: Check Your Trace
Using the Python fetch_item function from earlier, decide which of these calls returns a value and which raises an exception, then explain your answer in terms of the half-open range $[0, n)$: fetch_item(['x', 'y'], 0), fetch_item(['x', 'y'], 2), and fetch_item([], 0).
Hints
- For a collection of size n, the valid positions are $0, 1, \ldots, n-1$.
- Compute n with
len(items)for each call before testing the check. - An empty collection has n equal to 0, so the half-open range $[0, 0)$ contains no values.
Summary of Exception Handling Basics
- 1A precondition is an assumption a function relies on before it can work correctly, and the function should consider what its preconditions are and, where feasible, test them.
- 2The half-open range $[a, b)$ includes $a$ and excludes $b$, which makes it a natural fit for the valid positions $[0, n)$ of a collection of size $n$.
- 3A function can test its argument on entry and signal a violation with an exception instead of reading an invalid position, so the failure is reported where it occurs.
- 4A try block can be followed by several catch blocks; the exception type decides which handler runs. A bare throw; rethrows the caught exception, and std::
terminate()ends the program when the function cannot handle the exception.
At a glance
- A precondition is an assumption a function relies on before it can work correctly, and a violation is a refusal point rather than a result to guess at.
- The half-open range $[a, b)$ includes the start and excludes the end, so valid positions in a collection of size $n$ are $[0, n)$.
- Testing a precondition on entry and raising an exception stops normal work at the point of failure, so the expected return never happens.
- The exception type decides which catch block runs, a bare throw; rethrows the same exception, and std::
terminate()ends the program when the function is not designed to handle it.