Whenever you write a program that "goes into production" (people buy and use it), you probably expect that most of the time the program will start up, will have typical input, and will produce typical output. However, all software may be subject to misuse, whether deliberate or inadvertent, and whether by malicious or inexperienced users, and good software is written to anticipate and deal with the kinds of problems that may be caused by this kind of usage.

It is difficult, if not impossible, for a developer to anticipate all the different kinds of errors that may occur when a piece of software is being used. Sometimes a program is used without incident for years and then one day simply "comes off the rails" when a particular combination of input values that it has not seen before and is not prepared to deal with is entered. Ideally a program will always respond gracefully when something goes wrong, even if the error causes a complete shutdown of the program.

What should a developer's attitude toward errors be?

When writing code, a C++ developer must always be concerned with the errors that may occur in class methods, or in free functions, and how these errors should be dealt with. There are several possibilities:

  1. Simply deal with (overcome) the error where it occurs and then carry on.
  2. Print an error message and just carry on. Not a good idea, since the program may be in an unstable state.
  3. Print an error message and terminate the program. A very radical approach, especially if it's in a library routine that will be used by a lot of higher-level code in a variety of programs.
  4. Inform the caller that a problem has occurred and let the caller (perhaps with help from the user) respond to the problem. The caller may decide to handle the problem, terminate the program, or pass the problem up to a higher level for consideration.

The traditional way of dealing with potential program errors: if-statements

In earlier versions of high-level programming languages, error handling in programs was generally carried out using if-statements. The condition in the if was a check for an error of some kind, and the body of the if was the code for dealing with that particular error. The body of the else, if present, or the code following the if, would be the code containing the actions to be taken if that particular error did not occur. Depending on the number of potential errors being checked there could be a lot of if-statements, or a very large if-else construct simply to deal with a wide variety of possible errors, none of which occurred very often.

The problem with handling potential errors in this way is that the code for dealing with those errors is "mixed up" with the code for doing whatever the program is supposed to be doing most of the time. This causes "code bloat", and can make the reading and/or modifying of such programs much harder during the inevitable maintenance process.

We should make clear, however, that we do not wish to convey the impression that using if-statements to check for possible errors is "old-fashioned" and should no longer be employed. There are many situations, especially when only a few potential errors are being checked, that this approach is still perfectly adequate and the use of exception handling may be regarded as "overkill".

The modern way of dealing with potential program errors: exception handling

Most modern programming languages, including C++, have an "exception handling" mechanism for dealing with unusual or "exceptional" situations. In this case, as you might expect, there continues to be a check for possible errors (or "exceptional" situations), but instead of dealing with whatever has happened with code at the location where it happened, an "exception" is "thrown". This has the effect of transferring control to another program location, where the exception can be "caught" and dealt with at that location. This means that the exception-handling code for different exceptions can be grouped in a location away from the "normal" code, thus removing some of the "clutter" from the code doing the real work of the program and permitting "cleaner" code to be written.

The exception-handling mechanism requires, for each possible exceptional situation, that

  1. Definition of an appropriate exception type
  2. Throwing an instance of the exception type at the appropriate location (also called raising or generating an exception)
  3. Handling the exception

We should also point out that this is not necessarily the answer to every programmer's prayers either, and like any useful technology it can easily be used more than it should be, used in situations where it shouldn't be used at all, and used improperly in situations where it is (and should be) used. In fact, those of you who are familiar with, or who have at least heard of, the notorious "goto statement" of early versions of many programming languages may have an uneasy feeling that exception handling is somewhat reminiscent of that infamous demon from days gone by, and you would not be far wrong. In fact, whenever control in a program is suddenly transferred to a "distant" location in the source code, one may be asking for trouble, especially when trying to read the code in order to understand or debug it.

Exception-handling syntax in C++

Here is what a typical exception-handling setup looks like in C++, in generic form:

try
{
    //code in which a problem of some kind might occur
    if (some problem of a particular kind occurs)
        throw an instance of ExceptionType1 which identifies this problem
    //more code in which a problem of some kind might occur
    if (some problem of another particular kind occurs)
        throw an instance of ExceptionType2 which identifies this problem
    //more code ...
}
catch (ExceptionType1 e)
{
    //code for dealing with an exception of type ExceptionType1
}
catch (ExceptionType2 e)
{
    //code for dealing with an exception of type ExceptionType2
}
//more of these "catch blocks" if more exceptions may be thrown from the "try block"
...
//additional code that will be executed unless a catch block terminates the program

What we see here is the use of the three C++ keywords, namely try, throw, and catch, to form a construct for dealing with one or more possible exceptions.

The code that may give rise to the various possible exceptional circumstances is placed in a try block. Within that try block there will be tests for the various problems that may occur and, if a test proves positive, and exception will be thrown and caught by whichever catch block has a parameter matching the particular exception. This brief description oversimplifies the situation, of course, and you really need to see a program containing some exception-handling code in action to fully appreciate the above syntax and its effect. And of course there are some rules, guidelines and subtleties you should be aware of, and some of these are mentioned in the following section.

Notes on exception handling

  1. Planning for exceptions in your software should begin at the design stage, and the basic questions to ask include:
  2. The type of an exception may be something simple, like an int or an enum value, or an object of a class type. If it is the latter, then the thrown object may carry additional information about the exception, which can be supplied by parameters in the constructor for the thrown object.
  3. If the code within a try block completes executing and no exceptions have been thrown, all catch blocks following that try block are skipped and control transfers to the first statement following the last catch block.
  4. When an exception is thrown from within a try block, the catch blocks corresponding to that particular try block are tried in order until one with a matching exception is found. If this does not happen, the exception is passed up the "call stack" in a further attempt to find a handler. If no handler is found, the program terminates. Note, in this context, that a catch block of the form
    catch (...)
    {
        //code to handle whatever may be caught
    }
    
    can be used to catch an exception of any type, and thus can be used in a manner analogous to the default: option of a switch statement.
  5. The C++ language provides a hierarchy of exception classes, available from the headers <exception> and <stdexcept>. You may want to use the facilities available from these libraries if you are writing serious applications using C++. See here for some additional details.
  6. In all cases where one exception is a more general version of another kind of exception (often the case when using exceptions from a hierarchy of exception classes, for example), it is important to ensure that checking for the more specific exception takes place first.
  7. There are more details and subtleties to exception handling than we have given here. For example, you can nest try-catch blocks and you can specify in a function's prototype and function definition header what exceptions it throws. If you intend to use exception handling extensively, you should explore these aspects of the topic more thoroughly.