throw
, try
and catch
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:
To implement this capability, we have dedicated keywords within C++, and many similar languages: throw
, try
, and catch
.
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.
throw
and Unreachable CodeWhen 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); }
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.
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();
}
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
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.
In this lesson, we've introduced the essence of exception handling in C++ using throw
, try
, and catch
. The key points include:
throw
to generate exceptions.try
and catch
blocks for catching and handling exceptions.catch
blocks to handle different types of exceptions.Generally, we don’t want to be throwing primitive objects like strings and integers. Instead, we should use objects of a type dedicated to the purpose of representing errors.
The standard library comes with a range of such types, that all inherit from a base std::exception
class. We can also create our own exception types, specific to our use case. These types can also inherit from std::exception
if we wish.
This would give us a standardized, structured way of creating exceptions. We will cover this in the next lesson
throw
, try
and catch
This lesson provides an introduction to exceptions, detailing the use of throw
, try
, and catch
.
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.