9.14. An Introduction To Exception Handling

Review

We explored some basic error detection and avoidance strategies in a previous chapter. But those strategies were limited, and we concluded we needed a technique that programmers can't inadvertently overlook or intentionally ignore. No programming construct can prevent programmers from ignorantly or maliciously writing bad code. Nevertheless, exception handling provides programmers with a powerful tool for managing potential program failures.

Exceptions were not included in C++'s initial release1 and were an "experimental" feature in the book2 that became the ANSI base document for the language. The history of exceptions in C++ explains why, despite a significant evolution, they are less refined than in other languages like Java and C#. The lack of refinement notwithstanding, C++ exceptions are flexible and satisfy the previously established needs.

Application or Using CodeSupplier or Library Function
try
{
	function(...);
}
catch (exception)
{
	...
}
void function(...) noexcept(false)
{
	...
	if (...)
	{
		throw exception;
	}
}
The four keywords for robust exception handling. Exception handling is most appropriate when a function might encounter an error but can't determine a suitable response, so the function reports the error to another part of the program that can. Although exception handling isn't limited to a supplier/application architecture, this relationship helps illustrate why a function may choose to report an error rather than act on it. Library functions are general and typically written before the application programs that use them. In such cases, library functions cannot determine an appropriate error response and defer to the application to handle the error. The ellipses denote code omitted for brevity.
try
Programs call functions that might report an error by throwing an exception in a try block. try blocks may contain any valid code, including multiple function calls. If none of the called functions throws an exception, then the try block ends, and the program skips all the catch blocks. If a function call throws an exception, the program skips the statements following the failed function call to the end of the block. Execution resumes with catch blocks.
catch
One or more catch blocks follow the try block, and we design each to handle one kind of exception. The syntax of each catch-block is similar to a function: the parentheses enclose a data type and a local variable, and the braces enclose the statements that run if the block executes. The exception handling mechanism begins with the top catch block and works downwards until a catch block matches the thrown exception. If none of the catch blocks match the exception, then the program's behavior depends on its structure. If the try and catch blocks are in a function that was itself called in a try block, then the exception is passed to the next catch block. Otherwise, the program aborts.
throw
Functions create a throw statement with the throw keyword. Syntactically, throw statements are similar return-statement, requiring an expression between the keyword and the semicolon. Throw statements initiate the exception-handling process by throwing or launching an exception. Exceptions can be a fundamental type like an integer or a character pointer but are more often objects. Programs typically embed throw-statements in branches such as if, if-else, or switches. Functions end after throwing an exception, skipping all statements following the throw-statement.
noexcept
The noexcept keyword signals that a function does not throw any exceptions, while noexcept(false) signals that it can.

Unignorable Exceptions

In our previous exploration of basic error detection and avoidance strategies, we saw that library functions could return error codes or store them in global variables. But we also saw that it is impossible to force application programmers to check the codes, meaning that a program can experience an error but not respond to it. Programs that experience an error but continue running in a compromised condition can corrupt data or damage hardware. Minimally, compromised programs waste the user's time and the system's resources as they work on tasks they cannot complete. Exceptions allow library function programmers to implement a fail-fast strategy that may not be graceful, but it prevents programs from continuing to run in a compromised state.

Header FileLibrary Source Code FileApplication Program File
void function() noexcept(false);





 
void function() noexcept(false)
{
    cout << "Entering function" << endl;
    throw "Exception";
    cout << "Exiting function" << endl;
}
 
int main()
{
    cout << "Starting main" << endl;
    function();
    cout << "main ending" << endl;
    return 0;
}
An unignorable exception example. The code fragments demonstrate the minimal syntax needed to implement an error-reporting mechanism that an application cannot ignore. It's a good programming practice to include both instances of noexcept(false), highlighted in blue and red, but either one is sufficient to trigger the desired behavior. The function throws a C-string exception, highlighted in green, and the second cout statement in the function does not run. The application code calls function(), highlighted in yellow, but the function throws an exception, so the program aborts, and the second cout statement does not run. The system displays an error diagnostic when the program aborts, but its exact format depends on the operating system and the user interface (GUI or CLI).

Exceptions And Program Control

A program's flow of control is the sequence of operations or instructions that it runs. Previously, we employed an analogy to help us understand the flow of control. We imagined printing the program on a long, continuous strip of paper, placing a marker at the function main, and moving the marker from statement to statement as the program "runs." The line traced by the marker shows the branches the program follows and the loops of the iterative statements. The line breaks, begins anew, ends, and resumes with each function call. Understanding how exceptions impact a program's flow of control is essential for using them effectively.

Throw statements always interrupt a program's normal flow of control. Programs skip statements following a throw statement and a function call that throws an exception. As described in the previous figure, the program delivers uncaught exceptions to the runtime system, which aborts the program.

A graphical representation of the flow of control during exception handling. Two rectangles represent two code fragments:

Fragment 1:
try
{
    statements1;
    function1();
    statements2;
}
catch (const char* m)
{
    handler-statements;
}
statements3;

Fragment 2:
function1()
{
    statements4;
    if (cond)
        throw &dquot;Error&dquot;
    statements5;
}
A black arrow runs from function1() down to the second fragment. A red dashed arrow runs from the throw statement in the function up the catch statement in the first fragment. A graphical representation of the flow of control during exception handling. Three rectangles represent three code fragments:

Fragment 1:
try
{
    statements1;
    function1();
    statements2;
}
catch (const char* m)
{
    handler-statements;
}
statements3;

Fragment 2:
function1()
{
    statements4();
    function2();
    statements5;
}

Fragment 3:
function2()
{
    statements6;
    if (cond)
        throw &dquot;Errordquot;
    statements7;
}
A black arrow runs from function1() in the first fragment down to the second fragment. A second black arrow runs from function2() in the second fragment to the third fragment. A red dashed arrow runs from the throw statement in the third fragment up the catch statement in the first fragment.
(a)(b)
Exceptions and flow of control. In the illustrations, statements* represent groups of zero or more statements numbered to facilitate describing the program's behavior. The handler-statements represent the operations dealing with or handling an exception. Empty catch blocks3 can make debugging and validating a program difficult, so experienced programmers avoid the practice. cond indicates a logical condition that controls branching behavior. The solid black arrows denote function calls, while the dashed red arrows denote the flow of control when an exception is thrown.
  1. The flow of control in basic exception handling. If the code does not experience an error, the execution sequence is statements 1, 4, 5, 2, and 3. If function1 throws an exception, the sequence is statements 1, 4, the handler-statements, and 3.
  2. Program flow of control when a function does not catch an exception. If the code does not throw an exception, the execution sequence is statements 1, 4, 6, 7, 5, 2, and 3. If function2 throws an exception, the sequence is statements 1, 4, 6, the handler-statements, and 3. We can extend the illustrated behavior to function call sequences of any length: The exception handling mechanism automatically unwinds the call stack (2) until it reaches a function that can catch the exception or it aborts the program. As the exception handling system skips functions that can't catch the exception, it destroys the objects defined in the skipped functions, ensuring that the call stack is correct and preventing any memory leaks on the heap.

Fundamental Data Types As Exceptions

Programmers generally implement exceptions as objects and handle them in object-oriented programs. But in chapter 6, I claimed that we could use fundamental or primitive data types as exceptions and use them in non-object-oriented programs. Some of the detailed examples at the end of the chapter will use the simple exception handling illustrated in the following figure.

ApplicationLibrary
#include <iostream>
using namespace std;

void function1(bool) noexcept(false);		   // (a)
void function2(bool) noexcept(false);
void function3(bool) noexcept(false);
void function4(int) noexcept(false);


int main()
{
    cout << "Starting main" << endl;
    try
    {
        function1(true);			    // (b)
        function2(true);
        function3(true);
        function4(7);
    }
    catch (char code)				    // (c)
    {
        cerr << "Exception: " << code << endl;
    }
    catch (int code)				    // (d)
    {
        cerr << "Exception: " << code << endl;
    }
    catch (const char* message)			    // (e)
    {
        cerr << "Exception: " << message << endl;
    }
    cout << "main ending" << endl;

    return 0;
}
 
void function1(bool activate) noexcept(false)
{
    cout << "Entering function1" << endl;
    if (activate)
        throw 'X';				    // (c)
    cout << "Exiting function1" << endl;
}

void function2(bool activate) noexcept(false)
{
    cout << "Entering function2" << endl;
    if (activate)
        throw 7;				    // (d)
    cout << "Exiting function2" << endl;
}

void function3(bool activate) noexcept(false)
{
    cout << "Entering function3" << endl;
    if (activate)
        throw "Error in function3";		    // (e)
    cout << "Exiting function3" << endl;
}

void function4(int input) noexcept(false)	    // (f)
{
    cout << "Entering function4" << endl;
    switch (input % 2)
    {
        case 0:
            throw "input is even";
        case 1:
            throw "input is odd";
    }
    cout << "Exiting function4" << endl;
}
Fundamental-type exceptions. The example demonstrates exception handling syntax, how to use fundamental data types as exceptions, and how exceptions alter the program's normal flow of control. However, the program is unrealistic because it doesn't solve a problem or perform any significant operations to detect an error. More authentic examples will follow in this and subsequent sections of this chapter. A link to the complete program is located at the bottom of the page.
  1. The example puts the prototypes in the application for simplicity, but an authentic application/library architecture would put them in a header file.
  2. Programmers typically group multiple function calls in a single try block rather than putting each call in a try block by itself. The function arguments determine if the functions throw an exception or not: true causes an exception; function4 always throws an exception. Beginning with function1 and working downwards, compile and run the program and follow the execution path. Change the top true to false and repeat the cycle.
  3. function1 throws a character as an exception, which the program handles with the first catch block. The function could have multiple throw statements to represent different error conditions, and the catch block could include branching logic to respond appropriately to each possible code or condition.
  4. The same as (c) but with an integer.
  5. function3 throws a C-string that the catch block prints as a specific error message.
  6. function4 demonstrates simple branching logic (see Alternate representations of Boolean values) to illustrate that a function may have multiple throw statements. The cases don't need to end with the break keyword because the throw statement causes program control to leave the function. This example notwithstanding, programmers should not use exception handling to replace flow of control statements.
Together, try and catch blocks resemble flow of control statements like switches or if-else ladders. However, language designers intend for the exception-handling system to run infrequently, not as a part of routine data processing operations. Although "hidden" from programmers and users, the exception-handling system performs a significant amount of processing and runs an order of magnitude slower than the flow of control statements. Despite their similarities, exception handling and flow of control are not the same, have different purposes, and are generally not interchangeable.

Object-Oriented Exception Handling: class stack

Stacks are a common data structure used throughout general computing. Their ubiquity makes them a good candidate for inclusion in data structure libraries such as C++'s Standard Template Library and Java's Collections Framework. Their simplicity also makes them good programming examples. push and pop are the principal stack operations. If we implement a stack as a fixed-size array, what happens if we try to push too many data items onto the stack, exceeding its size? Or what happens if we try to pop data off an empty stack?

Stacks save and organize data while a program runs. The program risks losing the data if a push operation fails to save data because the stack is full. If a pop operation fails to return valid data because the stack is empty, the program may continue running in a compromised state. Secure, library-grade functions test for these boundary conditions and respond appropriately. While the last versions of the push and pop functions did test overflow and underflow and printed corresponding error messages, they didn't prevent the program from running in an insecure state. We're in a position now to make the stack class, particularly the push and pop functions, more secure and robust.

#include <iostream>
using namespace std;

class StackException
{
    private:
        const char* message = nullptr;
        int size = 0;

    public:
        StackException(const char* m) : message(m) {}
        StackException(const char* m, int s) : message(m), size(s) {}

        void display()
        {
           cerr << message << "\nstack size: " << size << endl;
        }
};
class Stack
{
    private:
        int size;
        int sp = 0;
        char* stack = nullptr;

    public:
        Stack(int s) : size(s), stack(new char[size]) {}

        void push(char c) noexcept(false)
        {
            if (sp < size)
                stack[sp++] = c;
            else
                throw new StackException("Stack Overflow", size);
        }

        char pop() noexcept(false)
        {
            if (sp > 0)
                return stack[--sp];
            else
                throw new StackException("Stack Underflow");
        }
};
int main()
{
    try
    {
        Stack s(3);

        s.push('A');
        s.push('B');
        s.push('C');
        s.push('D');	// overflow
        s.pop();
        s.pop();
        s.pop();
        s.pop();	// underflow
    }
    catch (StackException* se)
    {
        se->display();
    }

    return 0;
}



 
stack class with exceptions: Version 1. The first version of the stack class example creates a single exception class named StackException to represent both stack overflow and underflow errors. The exception class provides data members specifying the specific stack error an exception object represents. This version also illustrates throwing object pointers as exceptions. As illustrated, the program experiences an overflow error; in main, comment out the last push operation to cause an underflow error.
#include <iostream>
using namespace std;


class underflow
{
    public:
        void display() { cerr << "Stack underflow error" << endl; }
};
class overflow
{
    private:
        int size = 0;
    public:
        overflow(int s) : size(s) {}
        void display() { cerr << "Stack overflow error"
            "\nstack size: " << size << endl; }
};
class Stack
{
    private:
        int size;
        int sp = 0;
        char* stack = nullptr;

    public:
        Stack(int s) : size(s), stack(new char[size]) {}

        void push(char c) noexcept(false)
        {
            if (sp < size)
                stack[sp++] = c;
            else
                throw overflow(size);
        }

        char pop() noexcept(false)
        {
            if (sp > 0)
                return stack[--sp];
            else
                throw underflow();
        }
};
int main()
{
    try
    {
        Stack s(3);

        s.push('A');
        s.push('B');
        s.push('C');
        s.push('D');    // overflow
        s.pop();
        s.pop();
        s.pop();
        s.pop();    // underflow
    }
    catch (overflow of)
    {
        of.display();
    }
    catch (underflow uf)
    {
        uf.display();
    }

    return 0;
}
stack class with exceptions: Version 2. The second version of the stack class example creates two exception classes named underflow and overflow to represent the two stack errors. This version also demonstrates throwing objects as exceptions. As illustrated, the program experiences an overflow error; in main, comment out the last push operation to cause an underflow error. Please note that the overflow display function is correct: the preprocessor automatically concatenates the two C-strings (split to fit the column width), forming the single C-string it passes to the compiler component.

The choice between one or two exception classes is usually a matter of personal taste and programming style, but occasionally the problem might favor one over the other. Exceptions and how we use them in programs can be more sophisticated than the examples. We need to expand our understanding of object-oriented programming before taking advantage of their additional features. We'll revisit exception handling in subsequent chapters.

Downloadable Code

All programs are formatted with 4-character indentation.


1 Stroustrup, B. (1986). The C++ Programming Language. Reading, MA: Addison-Wesley.
2 Ellis, M. A. & Stroustrup, B. (1990). The Annotated C++ Reference Manual. Reading, MA: Addison-Wesley.
3 Empty catch blocks are called "so what" exception handlers because they don't do anything ("I caught the exception, so what?"). Catching the exception but failing to report it eliminates the information needed to identify when and where an error occurs and allows the program to continue running in an insecure state. Empty catch blocks are antithetical to the fail-fast design strategy. They can be more dangerous than not catching an exception and are rarely appropriate. I'm aware of a case where a software engineer was assigned a three-month project to correct the empty catch blocks left by another engineer departing the company "under a cloud" (i.e., in less than amicable circumstances).