Concepts / Throwing Exceptions in C++

Throwing Exceptions in C++

Throw an exception to indicate that a function cannot perform its assigned task.

  • Programming

Why a Function Sometimes Cannot Finish Its Job

Every function is written to do a job. Sometimes the job cannot be completed. The input may be invalid, a required resource may be missing, or a calculation may be meaningless. Good error handling is the discipline of deciding, in advance, how a program will respond in those moments.

This article covers three practices: throwing an exception when a task cannot be performed, developing an error-handling strategy early in the design, and declaring a function noexcept when it is not meant to throw. The main worked idea is a Vector whose subscript operator must refuse requests for positions that do not hold an element.

What do you think happens?

A Vector holds 5 elements, numbered 0 through 4. A program asks for element number 7. Before reading on, what do you think the Vector should do: quietly return something, repair the request, or report the problem?

Reveal answer
Answer:

It should report the problem by throwing an exception, so that the user can decide what to do next.

The source states that the implementer must detect the out-of-range access and inform the user. The request is neither quietly repaired nor quietly ignored.

The Test for Throwing

The key question is when a throw is appropriate. The source's guidance is to throw an exception to indicate that you cannot perform an assigned task. The trigger is therefore not that something looks unusual. The trigger is that the function genuinely cannot carry out what it was asked to do.

Consider a function that divides two numbers. If the divisor is zero, the function has no sensible result to return. Throwing is the way the function says that the task cannot be completed. It does not invent a fake answer that the caller might mistake for a real one.

Deciding on Error Handling Before the Code Exists

The source recommends developing an error-handling strategy early in a design. Error handling should not be an afterthought added once the rest of the program is written. When the design is still forming, the team decides which operations can fail, which failures mean the task cannot be performed, and how those failures will be signaled to the rest of the program.

This timing matters because the choice affects every function that depends on the class. If the Vector class has already been written with no agreed way to report failure, each later function has to invent its own response. Deciding the signaling method first gives every function one consistent channel.

Declaring a Function noexcept

The noexcept keyword appears after the parameter list in a function declaration. It is a statement of intent: this function is designed not to throw exceptions. The contrast the source draws is between functions that may not throw, which should be declared noexcept, and functions that may throw, which are not declared that way.

Declaring noexcept therefore tells readers of the code something about the function's behavior before they read its body. A caller who sees the keyword knows the function is not expected to report failure by throwing. A caller who sees no keyword must assume that a throw is possible.

Why the Vector Must Report Out-of-Range Access

Every container has a rule about which positions are legal. A Vector holds a fixed number of elements, and its subscript operator lets a program pick one of them by index. Asking for an index that does not correspond to a stored element is an error in the program that made the request.

The source states the design goal directly. The person who writes the Vector implementation is responsible for detecting an attempted out-of-range access. Detection alone is not enough. The implementer must then tell the user about the problem, and the user decides what action to take. The error is surfaced so that the code that caused it, or the code that calls it, can respond.

This separates two jobs. The Vector knows the valid range, because it knows its own size. The user knows what the program should do next. Throwing an exception is the channel through which the Vector hands the problem to the user without deciding on the user's behalf.

The Valid Range of Indices

A valid index names a stored element. If the Vector holds n elements, the valid indices run from 0 up to n minus 1, which can be written as $0 \leq i < n$. Any index below zero or at or above n is outside the range. The guard in the Vector's subscript operator is built around exactly this boundary.

index -1throwsindex 0returns elementindex 1returns elementindex n-1returns elementindex nthrows
For a Vector of n elements, which index positions are accepted, and which are rejected with a throw?

Read the strip from left to right. Indices in the middle of the strip are accepted and return an element. The index just below zero and the index equal to n both fall outside the range and both throw. The same boundary test covers both ends of the range.

The Guarded Subscript Operation

The guard in Vector's subscript operator tests the index before any element is accessed. It checks whether i is less than 0, or whether size() is less than or equal to i. A failing guard causes an out_of_range exception to be thrown. A passing guard returns a reference to the element.

cpp
Walk through it
  1. 1
    The function receives an index named i.
  2. 2
    The if tests two conditions joined by ||. If either is true, the condition is true.
  3. 3
    When the condition is true, the throw statement creates an out_of_range exception and the function stops without returning.
  4. 4
    When the condition is false, the function skips the throw and returns elem[i], a reference to the stored element.
Notes on the code
  • The if condition is the trigger: it is true when i is negative or when i is at or past the number of elements.
  • The throw statement is the action: it signals that the task cannot be performed by raising an out_of_range exception.
  • The return statement runs only when the guard has passed, so it never hands back a position that holds no element.

Tracing a Valid Access and a Rejected Access

Worked example

Tracing index 2 and index -1 through the guard

A Vector holds 5 elements. Trace what happens for the call v[2], and then for the call v[-1].

  1. 1
    Call with i = 2

    The function starts at the guard. The first test is i less than 0, which is 2 less than 0, so it is false.

  2. 2
    Second test for i = 2

    The second test is size() less than or equal to i. Here size() is 5, so 5 less than or equal to 2 is false.

  3. 3
    Guard decision for i = 2

    Both tests are false, so the whole condition is false. The throw is skipped and the function returns the element at position 2.

  4. 4
    Call with i = -1

    The function starts at the same guard. The first test is i less than 0, which is -1 less than 0, so it is true.

  5. 5
    Guard decision for i = -1

    The condition is already true. The second test is never needed for this input, so the throw runs and no element is returned.

Result

The call v[2] returns the element stored at position 2. The call v[-1] throws an out_of_range exception, and the divergence happens at the guard.

Every call enters the function at the same point and must pass the guard. Only one of the two outgoing paths is taken for a given index. The throw path is the only one that ends in an exception rather than a returned element. Watching the guard decide which branch runs is the moment where the behavior of a valid request and a rejected request separates.

The Guard as a Fork in the Road

The flowchart below shows the guard as a single decision with two outgoing paths. Every call starts at the top. The guard either lets control continue to the return statement, or sends control to the throw. The decision is made before any element is touched, so an invalid position never reaches the storage.

operator[](i)calledi < 0 or size() <= i?throw out_of_rangereturn elem[i]yesno
When operator[] is called with an index, which check runs first, and at what exact point do the valid-access path and the out-of-range throw path split apart?

Read the diagram as a fork. The guard is the only decision point. The yes branch ends in the throw, and the no branch ends in returning the element. The guard tests two separate conditions, and the first one covers indices below the valid range, so a negative index is rejected before the second test matters.

Mistakes Beginners Make with Throws

  • Common mistake

    Throwing whenever something looks unusual

    What it looks like

    A function throws because the input is an unusual but workable value, even though it could still complete its task.

    Why it fails

    The trigger for a throw is that the function cannot perform its assigned task, not that the input looks strange.

    Do this instead

    Throw only when the function genuinely cannot carry out what it was asked to do, and return the normal result otherwise.

  • Common mistake

    Repairing an out-of-range index silently

    What it looks like

    The subscript operator clamps index 7 to the last valid position and returns that element without telling the caller.

    Why it fails

    The source requires the implementer to inform the user, who then decides what action to take. A silent repair hides the problem.

    Do this instead

    Detect the out-of-range access and report it by throwing, so the caller can respond.

  • Common mistake

    Declaring noexcept on a function that may throw

    What it looks like

    A subscript operator that throws out_of_range is marked noexcept.

    Why it fails

    The noexcept declaration says the function is designed not to throw, which contradicts the function's actual behavior.

    Do this instead

    Declare noexcept only on functions meant not to throw. Leave the declaration off a function that may throw.

  • Common mistake

    Adding error handling after the rest of the class is finished, which forces each function to invent its own response.

Practice Tracing the Guard

EASY

A Vector holds 4 elements, numbered 0 through 3. For each call below, state whether the guard passes and returns an element or fails and throws. Then explain which of the two tests decided the outcome: v[0], v[3], v[4], and v[-2].

Hints
  • Start with the first test, i less than 0. If it is true, the second test is never needed.
  • For a Vector of 4 elements, size() is 4, so the second test asks whether 4 is less than or equal to i.
  • Indices 0 through 3 pass both tests. Indices below 0 or 4 and above fail.

Key Takeaways

  • A function should throw an exception when it genuinely cannot perform its assigned task, not merely when something looks unusual.
  • An error-handling strategy should be decided early in a design so that every later function reports failure through one consistent channel.
  • Declare a function noexcept only when it is designed not to throw, because the declaration communicates that promise to every reader and caller.
  • The guarded subscript tests i less than 0 or size() less than or equal to i before touching any element, so a failing guard throws out_of_range and a passing guard returns the element.

Related concepts

More concepts starting with T