Throwing Exceptions in C++
Throw an exception to indicate that a function cannot perform its assigned task.
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
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.
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.
- 1The function receives an index named i.
- 2The if tests two conditions joined by ||. If either is true, the condition is true.
- 3When the condition is true, the throw statement creates an
out_of_rangeexception and the function stops without returning. - 4When the condition is false, the function skips the throw and returns elem[i], a reference to the stored element.
- 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_rangeexception. - 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
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].
- 1Call 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.
- 2Second test for i = 2
The second test is
size()less than or equal to i. Heresize()is 5, so 5 less than or equal to 2 is false. - 3Guard 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.
- 4Call 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.
- 5Guard 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.
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.
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 likeA function throws because the input is an unusual but workable value, even though it could still complete its task.
Why it failsThe trigger for a throw is that the function cannot perform its assigned task, not that the input looks strange.
Do this insteadThrow 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 likeThe subscript operator clamps index 7 to the last valid position and returns that element without telling the caller.
Why it failsThe source requires the implementer to inform the user, who then decides what action to take. A silent repair hides the problem.
Do this insteadDetect 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 likeA subscript operator that throws out_of_range is marked noexcept.
Why it failsThe noexcept declaration says the function is designed not to throw, which contradicts the function's actual behavior.
Do this insteadDeclare 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
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 throwsout_of_rangeand a passing guard returns the element.