Exception Handling: Fundamentals
|
Learning
Objectives After completing this module,
students should be able to: •
Explain what an exception is and why exception handling is better
than traditional error codes. •
Use the try, throw and catch keywords and
describe the flow of control. •
Write multiple handlers and a catch-all handler, and state the
rules for matching a thrown value to a handler. •
Explain exception propagation and stack unwinding, including the
destruction of local objects. •
Use nested try blocks and
rethrowing, and explain what happens to an uncaught exception. |
1. Introduction
Programs do not always run as expected. A file may not exist, memory may
run out, a user may enter invalid data, a number may be divided by zero, or an
array index may go out of range. Such unusual events that occur while a program
is running are errors; the ones that the program can detect and respond
to are handled with exceptions.
|
Definition: An
exception is an abnormal condition or run-time error that occurs
during program execution and disturbs its normal flow. Exception handling
is the C++ mechanism that detects such a condition, transfers control to
special code written to deal with it, and lets the program continue or
terminate safely. |
1.1 Types of errors
|
Type
of error |
When
it is detected |
Example |
|
Syntax (compile-time) error |
During compilation |
Missing semicolon, undeclared variable |
|
Logical error |
Not detected automatically; the output is wrong |
Using + where * was intended |
|
Run-time error |
During execution |
Division by zero, invalid memory access, file not found |
Exception handling deals with run-time errors that the program can
anticipate. It does not repair syntax errors and does not find logical errors.
1.2 Traditional error handling and its problems
Before exceptions, errors were reported through return codes, global
error variables or exit(). These
methods have several drawbacks:
|
Problem |
Explanation |
|
Error code can be ignored |
The caller may forget to check the return value, and the
program continues with wrong data |
|
Error and result are mixed |
A function returning -1 for failure cannot also return -1 as a valid result |
|
Code becomes cluttered |
Every call must be followed by if tests, hiding the real logic |
|
Errors must be passed by hand |
Each function in a chain has to return the error upwards
itself |
|
Constructors cannot return values |
There is no way to report a failure from a constructor with
a return code |
|
Feature |
Traditional
(return codes) |
Exception
handling |
|
Error detection and handling |
Mixed with normal code |
Separated: normal code in try,
handling in catch |
|
Ignoring an error |
Easy to do by mistake |
An unhandled exception cannot be ignored: the program
terminates |
|
Passing error to a caller |
Manual, at every level |
Automatic (propagation) |
|
Clean-up of local objects |
Manual |
Automatic (stack unwinding) |
|
Readability |
Lower |
Higher |
1.3 Advantages of exception handling
1.
Separates the error-handling code from the
normal program logic.
2.
Propagates the error automatically to the code
that knows how to deal with it.
3.
Cleans up automatically: local objects are
destroyed while the exception travels outward.
4.
Works with constructors and operators, which
cannot return error codes.
5.
Allows different error types to be handled
differently, through different handlers.
2. Exception Handling
Fundamentals
C++ exception handling is built on three keywords:
|
Keyword |
Purpose |
|
try |
Encloses the code that may cause an exception (the code
being monitored) |
|
throw |
Signals ("raises") that an exceptional condition
has occurred and passes a value describing it |
|
catch |
Defines a handler that receives and processes an exception
of a given type |
try {
//
statements that may throw an exception
throw
value; // raises an
exception
}
catch (type1 parameter) {
// handler
for exceptions of type1
}
catch (type2 parameter) {
// handler
for exceptions of type2
}
•
A try
block must be followed immediately by at least one catch
handler.
•
The value given to throw
can be of any type: a built-in type, a string, or an object of a class.
•
The code that discovers the problem (throw) and the code that handles it (catch) may be in different functions.
|
Version Note: The
try,
throw
and catch
mechanism is the same in all C++ standards from C++98 to C++23. The features
that did change between standards (function exception specifications, unexpected(),
uncaught_exception())
are explained in Modules 5 and 6 with a version note each. |
2.1 The try block
The statements that may raise an exception are placed inside the braces
of a try block. If no exception
occurs, the block completes normally and all handlers are skipped. If an
exception is thrown, the rest of the block is abandoned and control goes to a
matching handler.
2.2 The throw statement
throw expression; creates an
exception object (a copy of the expression value) and starts the search for a
handler. Examples:
throw 100; // int
throw 'E'; // char
throw "Division by zero"; // const char *
throw runtime_error("File not found"); // an object of a class
2.3 The catch block
A catch block is a handler for one
type of exception. The parameter names the type that the handler accepts and
receives the thrown value. If the value is not needed, the parameter name can
be omitted. A handler for a class type should normally take the parameter by
reference to const, as in catch (const
runtime_error &e), to avoid copying and slicing (Module 4).
2.4 Program: division by zero
Program 3.1: Detecting and handling an
exceptional condition.
#include <iostream>
#include <stdexcept>
using namespace std;
int main()
{
int a = 10;
int b = 0;
try {
if (b
== 0)
throw runtime_error("Division by zero is not allowed");
cout
<< "Result: " << a / b << endl;
}
catch
(const runtime_error &e) {
cout
<< "Exception caught: " << e.what() << endl;
}
cout
<< "Program continues normally." << endl;
return 0;
}
Output:
Exception caught: Division by zero is not allowed
Program continues normally.
1.
The statements in the try
block are executed. Because b == 0,
throw creates a runtime_error object carrying the message.
2.
The remaining statement of the block (cout << a / b) is skipped, so the dangerous
division is never performed.
3.
The catch
block receives the object; e.what()
returns the message text.
4.
After the handler, execution continues with the first
statement after the try-catch
construct, so “Program continues normally.” is printed.
3. Flow of Control

Figure 3.1: Flow
of control in a try-catch construct.
1.
No exception: the entire try
block executes, every catch
block is skipped, and execution continues after the last handler.
2.
Exception thrown: the remaining statements of
the try block are skipped. The local
objects created in the block are destroyed.
3.
The handlers are examined in the order in which they
are written; the first one that matches is executed, and the others are
ignored.
4.
After the handler finishes, control passes to the
statement following the handlers. It does not return to the point
of the throw: C++ uses the termination
model of exception handling.
5.
If no handler matches, the exception moves to the
calling function (Section 5). If no function handles it, the program terminates
(Section 7).
4. Multiple catch
Blocks and Handler Matching
A try block can be followed by
several handlers, one for each type of exception that may arise. The special
handler catch (...) (the catch-all
handler) accepts an exception of any type; it is normally written
last.
Program 3.2: Handlers for int, char
and any other type.
#include <iostream>
using namespace std;
int main()
{
int choice;
cout
<< "Enter 1 (int), 2 (char) or 3 (other): ";
cin
>> choice;
try {
if
(choice == 1)
throw 100;
else if
(choice == 2)
throw 'E';
else if
(choice == 3)
throw 3.14;
else
cout << "No exception thrown." << endl;
}
catch (int
e) {
cout
<< "Integer exception caught: " << e << endl;
}
catch (char
e) {
cout
<< "Character exception caught: " << e << endl;
}
catch (...)
{
cout
<< "Some other exception caught." << endl;
}
cout
<< "Execution completed." << endl;
return 0;
}
Output for the three inputs 1, 2 and 3:
Enter 1 (int), 2 (char) or 3 (other): 1
Integer exception caught: 100
Execution completed.
Enter 1 (int), 2 (char) or 3 (other): 2
Character exception caught: E
Execution completed.
Enter 1 (int), 2 (char) or 3 (other): 3
Some other exception caught.
Execution completed.
For any other input (for example 9) the program prints No exception thrown.; the handlers are not used.
•
throw 100;
has type int and is received by catch (int e).
•
throw 'E';
has type char and is received by catch (char e).
•
throw 3.14;
has type double; no handler has that type,
so catch (...) receives it.
4.1 Rules for matching
|
Rule |
Explanation |
|
Exact type match |
The handler type must be the same as the thrown type
(ignoring const and references). |
|
No ordinary conversions |
A thrown int is not converted to double or long; a thrown char does not match int; a thrown const char * does not match string. |
|
Derived to base is allowed |
A handler for a base class also receives objects of its
derived classes (Module 4). |
|
First match wins |
Handlers are tried in order; later handlers are not
considered once one has matched. |
|
catch
(...) |
Matches everything, so it must be the last handler. |
Program 3.3: The thrown type decides which
handler runs; no conversion is applied.
#include <iostream>
#include <string>
using namespace std;
/* The type of the thrown value must match the
handler type;
ordinary
conversions (such as int to double) are NOT applied. */
void test(int which)
{
try {
if
(which == 1) throw 10; // int
if
(which == 2) throw 'x'; // char
if
(which == 3) throw "text";
// const char *
if
(which == 4) throw string("s");
// std::string
}
catch
(double d) { cout << "double handler: " << d <<
endl; }
catch (int
i) { cout << "int handler:
" << i << endl; }
catch
(const string &s) { cout << "string handler: " << s
<< endl; }
catch
(...) { cout <<
"catch-all handler (no exact match)" << endl; }
}
int main()
{
for (int i
= 1; i <= 4; i++)
test(i);
return 0;
}
Output:
int handler: 10
catch-all handler (no exact match)
catch-all handler (no exact match)
string handler: s
The value 10 is an int and is received by the int
handler although a double handler is
written before it (it does not match). The character 'x'
and the C-string "text"
match neither int, double
nor string, so they reach the
catch-all handler. Only the real string
object matches the string handler.
|
Note: Because a C string
literal has type const char *, throw
"message"; is caught by catch
(const char *msg) and not by catch
(const string &). For messages, throw a std::runtime_error
or another class from <stdexcept>. |
5. Exception Propagation and
Stack Unwinding
5.1 Propagation
A function that throws an exception does not need to handle it. If there
is no matching handler in the function, the exception is passed to the function
that called it, then to that function’s caller, and so on, until a
matching handler is found. This is called exception propagation.
Program 3.4: An exception thrown in functionB() and handled in main().
#include <iostream>
using namespace std;
void functionB()
{
cout
<< "Inside functionB()" << endl;
throw 100;
}
void functionA()
{
cout
<< "Inside functionA()" << endl;
functionB();
cout
<< "functionA() ends" << endl; // never executed
}
int main()
{
try {
functionA();
}
catch (int
e) {
cout
<< "Exception caught in main(): " << e << endl;
}
cout
<< "Program continues." << endl;
return 0;
}
Output:
Inside functionA()
Inside functionB()
Exception caught in main(): 100
Program continues.
Figure 3.2: The
exception leaves functionB() and functionA() and is caught in main().
The statement “functionA() ends” is never printed: after throw 100 the remaining code of both functions is
skipped.
5.2 Stack unwinding
While the exception travels back through the active functions, every
function that is left is removed from the call stack. The local (automatic)
objects of each function are destroyed on the way, and their destructors
are called in the reverse order of construction. This process is called stack
unwinding.
Program 3.5: Destructors run automatically
during stack unwinding.
#include <iostream>
#include <string>
using namespace std;
class Demo
{
string
name;
public:
Demo(const
string &n) : name(n) { cout << "Constructor: " <<
name << endl; }
~Demo() {
cout << "Destructor : " << name << endl; }
};
void inner()
{
Demo
d3("d3 (in inner)");
cout
<< "inner(): throwing" << endl;
throw 10;
}
void outer()
{
Demo
d1("d1 (in outer)");
Demo
d2("d2 (in outer)");
inner();
cout
<< "outer() ends" << endl; // never executed
}
int main()
{
try {
outer();
}
catch (int
e) {
cout
<< "Caught in main(): " << e << endl;
}
return 0;
}
Output:
Constructor: d1 (in outer)
Constructor: d2 (in outer)
Constructor: d3 (in inner)
inner(): throwing
Destructor : d3 (in inner)
Destructor : d2 (in outer)
Destructor : d1 (in outer)
Caught in main(): 10

Figure 3.3: Stack
unwinding: d3 is destroyed first, then d2, then d1, then the handler runs.
1.
d1 and d2 are created in outer()
and d3 in inner().
2.
throw 10
in inner() has no handler there. The
function is left and ~d3 runs.
3.
The exception reaches outer(),
which has no handler either: d2 is
destroyed, then d1 (reverse order
of creation).
4.
The handler in main()
finally runs.
|
Important: Stack unwinding
makes exceptions safe for resources: a resource owned by an object (a file, a
lock, memory in a smart pointer) is released by its destructor even when an
exception passes through. Memory obtained with a raw new
and released later with delete would be leaked
(Module 6, RAII). |
6. Nested try
Blocks and Rethrowing
A try block may contain another try block. An exception is first looked for in the inner
handlers; if none matches, the search moves outward. A handler can also decide
that it cannot fully deal with an exception and rethrow it with a throw; statement that has no operand. The same exception
object then continues to the outer handlers.
Program 3.6: Inner handler rethrows to the
outer handler.
#include <iostream>
using namespace std;
int main()
{
try {
cout
<< "Outer try block" << endl;
try {
throw 20;
}
catch
(int e) {
cout << "Inner handler: " << e << endl;
throw;
// rethrow the same exception
}
}
catch (int
e) {
cout
<< "Outer handler: " << e << endl;
}
return 0;
}
Output:
Outer try block
Inner handler: 20
Outer handler: 20
The inner handler performs its part of the work (printing a message) and
passes the exception on; the outer handler completes the handling. A bare throw; is valid only while a handler is active; used
elsewhere it calls std::terminate().
Rethrowing, and the difference between throw;
and throw e;, are discussed in Module
4.
7. Uncaught Exceptions
If an exception is thrown and no function on the call stack has a
matching handler, it is an uncaught exception. C++ then calls the
function std::terminate(), which by
default ends the program abnormally by calling abort().
Whether the stack is unwound before this happens is implementation-defined.
Program 3.7: No handler for a thrown
exception.
#include <iostream>
using namespace std;
int main()
{
cout
<< "Before throw" << endl;
throw
42; //
no handler anywhere
cout
<< "After throw" << endl;
return 0;
}
Output with GCC (the exact text of the
message depends on the compiler; the shell then reports that the program was
aborted, for example Aborted (core dumped)):
Before throw
terminate called after throwing an instance of 'int'
“After throw” is never printed. The functions terminate(),
set_terminate() and the related
features are studied in Module 5. A well-written program therefore handles
every exception that can occur, or at least installs a catch-all handler in main().
8. Rules, Limitations and Common
Mistakes
|
Mistake |
Correction |
|
Writing try without a catch |
Every try must be followed by at least one
handler |
|
Expecting throw 5 to match catch (double) |
The types must match; no conversion is applied |
|
Placing catch
(...) before other
handlers |
The catch-all handler must be last |
|
Expecting execution to resume after the throw |
Control continues after the handlers (termination model) |
|
Using exceptions for ordinary control flow |
Exceptions are for exceptional conditions, not for normal
loops or tests |
|
Throwing a string literal and catching string |
Throw std::string or runtime_error, or catch const char * |
|
Using a bare throw; outside a handler |
It calls terminate(); use it only inside a catch block |
|
Ignoring clean-up of resources |
Let objects (destructors, smart pointers) own the resources |
Exceptions have a run-time cost when they are thrown, so they should not
be used for conditions that occur routinely; for example, reaching the end of a
loop.
9. Quick Revision
•
try
monitors code, throw raises the exception, catch handles it.
•
Handlers are matched in order by exact type (or
derived-to-base); catch (...) matches
anything and goes last.
•
After a handler, control continues after the last
handler (termination model).
•
An unhandled exception propagates to the callers; if
nobody handles it, terminate() is
called.
•
Stack unwinding destroys local objects in reverse order
of construction.
•
throw;
rethrows the current exception from inside a handler.
10. Important Examination
Questions
Short-answer questions
1.
What is an exception? What is exception handling?
2.
Name the three keywords used in exception handling and
state their purpose.
3.
Why is exception handling better than returning error
codes?
4.
What is the termination model of exception handling?
5.
What is catch (...)?
Where must it be placed?
6.
What is exception propagation? What is stack unwinding?
7.
What happens when an exception is not caught?
8.
How is an exception rethrown?
Long-answer and
programming questions
1.
Explain the exception handling mechanism of C++ with a
program and a diagram of the flow of control.
2.
Write a program that throws and catches an int, a char
and a double exception using multiple
handlers.
3.
Explain with an example how an exception propagates
through several function calls.
4.
Explain stack unwinding and show with a program how
destructors are called.
5.
Write a program with nested try
blocks and rethrowing.
6.
Discuss the advantages and limitations of exception
handling.
No comments:
Post a Comment