Concepts / Exception Handling Basics

Exception Handling Basics

If an exception is raised, its type decides which catch block receives it.

  • 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…

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?

  • Read position 10 anyway and return whatever is there

  • Return the first item as a default

  • Refuse the request and signal the problem with an exception instead of reading the position

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

Definition

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.

0first item, included1included2last item, included3n, excluded
For a collection of size n = 3, which positions are valid, and why is the upper bound excluded?

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.

Worked example

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.

  1. 1
    Identify the start

    The square bracket on the left means $a = 2$ is included.

  2. 2
    Identify the end

    The round parenthesis on the right means $b = 6$ is excluded.

  3. 3
    List the values

    Starting at 2 and stopping before 6 gives 2, 3, 4, and 5.

  4. 4
    Count

    The range holds $b - a = 4$ values.

Result

The range $[2, 6)$ includes 2, 3, 4, and 5, and excludes 6.

Formula, step by step
  1. 1
    The position $i$ is the value the caller passes in.
  2. 2
    The left inequality $0 \leq i$ says the lower bound is included, so position zero is allowed.
  3. 3
    The right inequality $i < n$ says the upper bound $n$ is excluded, so position $n$ is not allowed.
  4. 4
    Together 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.

Call with positioniIs 0 <= i < n ?precondition checkReturn items[i]normal returnRaise IndexErrornormal return never happensyesno
After the precondition check runs, which path does a call take, and where does the normal return never happen?

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.

python
Output (expected)
b
Traceback (most recent call last):
  ...
IndexError: position 10 is outside the range [0, 3)
Notes on the code
  • 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.

cpp
Notes on the code
  • 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.
catch length_errormatches, runscatch length_errorskippedcatch bad_allocskippedcatch bad_allocmatches, runs
When the try block throws a given exception type, which catch block matches it, and which one is skipped?

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.

cpp
Notes on the code
  • The bare throw; inside the length_error handler rethrows the same exception after its own work.
  • The bad_alloc handler calls std::terminate() instead of continuing.
  • Control does not finish normally after the length_error catch 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.

Worked example

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?

  1. 1
    Raise

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

  2. 2
    Match

    The first catch names std::length_error, so it matches. The bad_alloc handler is skipped.

  3. 3
    Work

    The handler prints its message for the negative size case.

  4. 4
    Rethrow

    The bare throw; executes, so the same exception leaves the handler and propagates further.

Result

The length_error handler runs, prints its message, and the exception continues upward rather than finishing normally.

Worked example

Trace a bad_alloc through the same function

The try block raises std::bad_alloc. Which handler runs, and how does the program end?

  1. 1
    Raise

    Control leaves the try block at the point of the raise.

  2. 2
    Match

    The length_error handler does not match, so it is skipped. The bad_alloc handler matches.

  3. 3
    Terminate

    The handler calls std::terminate(), so the program ends.

Result

The bad_alloc handler runs and the program ends. No rethrow occurs and nothing further up is reached.

MEDIUM

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 like

    Returning a default item when the position is 10 in a three-item collection.

    Why it fails

    The function would be guessing at what the caller meant, which hides the misuse instead of reporting it.

    Do this instead

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

    Believing that [0, 3) contains 3 because the range goes up to 3.

    Why it fails

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

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

    Expecting the bad_alloc handler to run when the try block raised std::length_error.

    Why it fails

    Each catch names a single exception type, and only the handler whose type matches receives the exception.

    Do this instead

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

    Calling std::terminate() in a handler that should have rethrown the error so an outer function could deal with it.

    Why it fails

    std::terminate() ends the program, so the error is never passed on. A bare throw; passes the same error further.

    Do this instead

    Use 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

MEDIUM

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

  1. 1
    A 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.
  2. 2
    The 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$.
  3. 3
    A 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.
  4. 4
    A 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.

Related concepts

More concepts starting with E