Exceptions: throw
, try
and catch
This lesson provides an introduction to exceptions, detailing the use of throw
, try
, and catch
.
In the previous lesson, we introduced the assert()
macro, which gives us a way to check for errors at run time. This is useful for simple scenarios but, for more complex software, we also need a more powerful option. Specifically, we'd like to be able to do things like:
- Detect and recover from errors, potentially elsewhere in the call stack - that is, having our functions throw an error that is detected within one of the calling functions
- Generate errors that are represented by full-fledged objects. These give error handling code the ability to fully understand and react to the errors, by accessing variables and calling functions on the error object.
To implement this capability, we have dedicated keywords within C++, and many similar languages: throw
, try
, and catch
.
Throwing Exceptions with throw
Previously, we wrote this function using assert()
:
#include <cassert>
int Divide(int x, int y) {
assert(y != 0 && "Cannot divide by zero");
return x/y;
}
int main() {
Divide(5, 0);
}
Running this program, our output may have looked something like this:
Assertion failed: y != 0 && "Cannot divide by zero", file main.cpp
Let's rewrite this example using throw
instead:
#include <iostream>
int Divide(int x, int y) {
if (y == 0) throw "Cannot divide by zero";
return x/y;
}
int main() {
Divide(5, 0);
}
Our output log might now say something like this:
Unhandled exception in cpp.exe
An error that is generated using the throw
keyword is referred to as an exception. An unhandled exception (sometimes also called an uncaught exception) is simply an exception that our code hasn't detected and recovered from. We cover how to detect (or catch) exceptions later in this section.
Unreachable Code and throw
When a throw
expression is encountered, the flow of our application is disrupted in a similar way to when a function encounters a return
statement.
The main effect of this is that we can have unreachable code.
This is demonstrated in the following example, where our logging statement can never be executed:
#include <iostream>
int Divide(int x, int y) {
if (y == 0) {
throw 0;
// Unreachable code
std::cout << "Cannot divide by 0";
}
return x / y;
}
int main() { Divide(5, 0); }
Catching Exceptions with try
and catch
In any function of our program, we can introduce code that can detect exceptions. The syntax is comprised of two components - a try
block, and a catch
block.
Within the try
block, we place the code that might throw
an exception. Within the catch
block, we place the code that will be called if an exception was indeed thrown.
Below, we show a basic example of the syntax in action:
#include <iostream>
int main() {
try {
throw "Some Error";
} catch (...) {
std::cout << "I caught an error";
}
std::cout
<< "\nThe program can continue as normal";
}
Now, our throw
statement doesn't immediately terminate our program. Instead, we execute the code within the catch
block, and then our program continues as normal:
I caught an error
The program can continue as normal
More usefully, our try
block will contain calls to other functions. If those calls result in a throw
statement, the exception will bubble up the call stack until it finds a catch
statement to handle it:
#include <iostream>
int Divide(int x, int y) {
if (y == 0) throw "Cannot divide by zero";
return x / y;
}
int main() {
try {
Divide(5, 0);
} catch (...) {
std::cout << "I caught an error";
}
std::cout
<< "\nThe program can continue as normal";
}
I caught an error
The program can continue as normal
As with other block statements within functions, we can elect to return early from a catch
statement:
#include <iostream>
int Divide(int x, int y) {
if (y == 0) throw "Cannot divide by zero";
return x / y;
}
int main() {
try {
Divide(5, 0);
} catch (...) {
std::cout << "I caught an error";
return -1;
}
std::cout << " but the program recovered";
}
I caught an error
cpp.exe (process 34344) exited with code -1.
Exceptions and Memory Leaks
Because of the effect of throw
on control flow, exceptions can be a common cause of memory leaks. This simple program has a memory leak if SomeFunction()
can ever throw an exception:
void MyFunction() {
int* MyInt { new int { 42 } };
SomeFunction();
// This may not be executed
delete MyInt;
}
We could deal with this by wrapping our SomeFunction()
call in a try-catch block. Alternatively, this is yet another scenario where using managed pointers can make our lives easier:
#include <memory>
void MyFunction() {
auto MyInt{std::make_unique<int>(42)};
SomeFunction();
}
Smart Pointers and std::unique_ptr
An introduction to memory ownership using smart pointers and std::unique_ptr
in C++
Catching Specific Exceptions
Within the above example, the ...
syntax within catch (...)
indicates we want this catch
statement to catch all exceptions.
In more complex programs, this is generally not what we want - typically, our catch
block will only want to catch the types of exceptions it can handle.
Specifying the type of exception we want to catch
gives us the added benefit of being able to inspect the exception that was thrown like any other variable.
We can throw any type of object as an exception.
In this case, we threw a string - the literal expression: "Cannot divide by zero"
. As indicated in the output when we fail to catch this exception, this is an instance of const char*
, ie, a C-style array of characters.
We cover char*
in more detail later in the course but, for now, we can just consider it to be a type of string.
To catch exceptions of this specific type, we update our catch
code to indicate it is only interested in const char*
objects:
try {
// ...
} catch (const char*) {
// ...
}
If we want to access the exception within our catch
block, we simply give it a name and use it like any other variable:
#include <iostream>
int Divide(int x, int y) {
if (y == 0) throw "Cannot divide by zero";
return x / y;
}
int main() {
try {
Divide(5, 0);
} catch (const char* e) {
std::cout << "I caught an error:\n";
std::cout << e;
}
std::cout << "\nRecovered Successfully";
}
I caught an error:
Cannot divide by zero
Recovered Successfully
Catching Multiple Exception Types
We can catch
multiple distinct exception types by using multiple catch
statements:
#include <iostream>
int Divide(int x, int y) {
try {
if (y == 1) throw "Hi";
if (y == 2) throw -1;
if (y == 3) throw true;
} catch (const char* E) {
std::cout << "Caught a const char*: " << E;
} catch (int E) {
std::cout << "\nCaught an int: " << E;
} catch (...) {
std::cout << "\nCaught something else";
}
return x / y;
}
int main() {
Divide(5, 1);
Divide(5, 2);
Divide(5, 3);
}
Caught a const char*: Hi
Caught an int: -1
Caught something else
In this example, the const char*
error thrown by the first call to Divide()
will be caught by the first catch
statement.
The int
error thrown by the second call to Divide()
will be caught by the second catch
statement.
Any other type will be caught by the catch(...)
statement, including the bool
thrown by the third call to Divide()
.
This pattern of having a final catch(...)
block to capture anything that is not handled by more specific catch blocks is fairly common. Because of this, catch(...)
is sometimes referred to as the default handler.
Summary
In this lesson, we've introduced the essence of exception handling in C++ using throw
, try
, and catch
. The key points include:
- Understanding the use of
throw
to generate exceptions. - Implementing
try
andcatch
blocks for catching and handling exceptions. - Differentiating between catching specific exception types and using a catch-all handler.
- Practical application of multiple
catch
blocks to handle different types of exceptions.
Exception Types
Gain a thorough understanding of exception types, including how to throw and catch both standard library and custom exceptions in your code