Exceptions: throw, try and catch

This lesson provides an introduction to exceptions, detailing the use of throw, try, and catch.
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
3D Character Concept Art
Ryan McCombe
Ryan McCombe
Updated

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.

throw and Unreachable Code

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();
}

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 and catch 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.

Preview of Next Lesson: Exception Types

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

Was this lesson useful?

Next Lesson

Exception Types

Gain a thorough understanding of exception types, including how to throw and catch both standard library and custom exceptions in your code
3D Character Concept Art
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Exceptions and Error Handling
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 124 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Exception Types

Gain a thorough understanding of exception types, including how to throw and catch both standard library and custom exceptions in your code
3D Character Concept Art
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved