throw
, try
and catch
in C++throw
, try
and catch
statements to let our program manage unexpected situationsAssertions give us a way to check for errors, but they have two limitations:
To address these two limitations, we have dedicated key words within C++, and many similar languages: throw
, try
and catch
.
throw
Previously, we wrote this function using assert
:
#include <iostream>
#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 crash dump would have looked something like this:
main.cpp:5: int Divide(int, int):
Assertion `y != 0 && "Cannot divide by zero"' failed.
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 crash dump now looks like this:
terminate called after throwing an instance of 'char const*'
Our program terminates at the same point, although the error message is now less helpful. However, the benefit of using throw
is that we can catch those errors, and recover from them.
An error that occurs in software is frequently called an exception. Using the throw
keyword in this way is often referred to as “throwing an exception”
try
and catch
In any function of our program, we can introduce an error boundary. Error boundaries catch any errors that were thrown as the result of executing a block of code.
They are 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.
int main() {
try {
Divide(5, 0);
} catch (...) {
std::cout << "An error was caught";
}
std::cout << " but the program recovered";
}
An error was caught but the program recovered
As with most block statements within functions, we can elect to return early from a catch
 statement:
int main() {
try {
Divide(5, 0);
} catch (...) {
std::cout << "An error was caught";
return -1;
}
std::cout << " but the program recovered";
}
An error was caught
Within the above example, the ...
syntax within catch (...)
indicates we want to catch all errors. This is generally not what we want - typically, we will want to catch errors of specific types. This gives us the added benefit of being able to access the actual exception that was thrown and inspect it.
We can throw any type of object as an exception.
In this case, we threw a simple primitive - the literal expression: "Cannot divide by zero"
. As indicated in the crash dump, this is an instance of const char*
, ie, a C-style array of characters.
To access this object, we update our catch
code to indicate it is only interested exceptions that have a type of const char*
.
We also give that object a name so we can access it within the block of our catch
statement. In this example, we called it simply Error
:
int main() {
try {
Divide(5, 0);
} catch (const char* Error) {
std::cout << "An error was caught: "
<< Error << std::endl;
}
std::cout << "But the program recovered";
}
An error was caught: Cannot divide by zero
But the program recovered
We can catch
multiple distinct exception types by using multiple catch
 statements:
int Divide(int x, int y) {
if (y == 0) throw "Cannot divide by zero";
if (y == 1) throw -1;
if (y == 2) throw true;
return x/y;
}
int main() {
try {
Divide(5/0);
Divide(5/1);
Divide(5/2);
} catch (const char* Error) {
// A const char* was thrown
} catch (int Error) {
// An int was thrown
}
}
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.
The bool
error thrown by the third call to Divide
will not be caught. This is frequently referred to as an “uncaught exception”. If our program throws an exception that is never caught, it will terminate.
In this case, the crash dump might report something like:
terminate called after throwing an instance of 'bool'
Generally, we don’t want to be throwing arbitrary objects like strings and integers. Instead, we should use a type of object dedicated to this purpose.
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, by creating our own classes that inherit from std::exception
This would give us a standardized, structured way of creating exceptions. We will cover this in the next lesson
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.