Skip to main content

Object oriented programming

[C++] OOPs - Exception Handling

Picture this: you’re a software engineer working on a banking application, and you’ve been tasked with building a feature that allows users to withdraw money from their accounts. The app is up and running, and it’s pretty smooth—users can check balances, make transfers, and even withdraw funds.

One day, a user tries to withdraw $500 from their account, but they only have $300. What happens next? The withdrawal fails, but what happens in the system? Does it just break silently? Do you get an alert about the issue? Does the user get any feedback?

In the case of poorly designed software, this could happen: the user may see no error, and you, the developer, may have no idea there was even a problem. It might seem like the system just "swallowed" the problem, leaving everything unclear and messy.

But what if, instead of allowing that to happen, your system knew exactly when something was wrong—like an overdraft—and then threw an error that could be caught somewhere else in the system? Now, when the problem occurs, it’s not ignored; it’s handled properly, with a clear message telling the user what went wrong.

This is where exception handling comes into play. Exception handling allows us to deal with unexpected situations in a structured, predictable way. Rather than letting our program fail abruptly, we can gracefully handle errors, notify the user, and even recover from the error if possible. This is what makes throwing and catching exceptions in C++ such a critical tool in building resilient software.

But why do we need exception handling in the first place? What exactly is it, and when should we use it? Let's dive in.


Section 1: Throwing Exceptions

What Happens When Things Go Wrong?

Imagine you're cooking dinner. You follow the recipe to a T, but then, disaster strikes: you're out of eggs. What do you do? You could try to improvise, but chances are the recipe won’t turn out the way it’s supposed to. Or you might ask a friend, “Can you run to the store and get eggs?” You’ve passed the problem off to someone else who can handle it.

In the programming world, we handle problems in a similar way using exceptions. When something goes wrong—whether it's invalid input, a missing file, or a calculation error—you throw an exception. This essentially says, "I can't handle this right now, so I’m passing the problem along to someone who can."

In C++, throwing an exception is as simple as using the throw keyword. This allows us to raise an error and immediately transfer control to another part of the program that is set up to handle it.

Why Use throw?

But why do we even need to use throw? Why not just deal with errors immediately when they happen? The reason is simple: errors may happen in various parts of the program, and it might be inefficient or unmanageable to handle every potential error right where it occurs. Instead, throwing an exception allows us to centralize error-handling logic and keep the program running smoothly.

Let’s take a simple example: dividing two numbers. You can’t divide by zero, right? But if you don’t handle this situation, your program will either crash or give incorrect results. Throwing an exception allows you to handle this error cleanly:

#include <iostream>
#include <stdexcept>  // Standard exception library

void divide(int a, int b) {
    if (b == 0) {
        throw std::invalid_argument("Cannot divide by zero.");  // Throwing an exception
    }
    std::cout << "Result: " << a / b << std::endl;
}

int main() {
    try {
        divide(10, 0);  // This will throw an exception
    } catch (const std::exception& e) {
        std::cout << "Error: " << e.what() << std::endl;  // Handling the exception
    }
    return 0;
}

Code breakdown:

  • throw std::invalid_argument("Cannot divide by zero.");: We’re throwing an exception when the divisor is zero.
  • catch (const std::exception& e): We catch the exception in a catch block and print the error message to the user.

Here’s a key takeaway: throwing an exception helps us separate the actual logic of our program (like division) from error-handling logic. By using exceptions, we maintain a cleaner, more manageable codebase.

Intuition Behind Throwing Exceptions

Imagine you're in a relay race, running and passing the baton to the next runner. If something goes wrong—say, you trip—rather than continuing to run, you pass the baton (the problem) to someone else who can fix it.

In programming, when something goes wrong, you “throw” the error to the part of the program best equipped to handle it. This avoids letting the error ruin everything else in the program.


Section 2: Catching Exceptions

What Happens After You Throw an Exception?

So, you’ve thrown an exception—now what? When an exception is thrown, the program immediately transfers control to the nearest catch block that can handle the type of exception thrown.

Think of this like throwing a ball to someone. If they have a mitt (catcher’s mitt), they can catch the ball and deal with it. If no one is ready to catch the ball, it just falls to the ground, causing an error that could potentially crash the program.

How Does catch Work?

The catch block is the part of the code that catches the thrown exception and allows the program to continue. If you don’t have a catch block, the program will crash when it encounters an uncaught exception.

#include <iostream>
#include <stdexcept>

void riskyFunction() {
    throw std::out_of_range("Out of range error!");  // Throwing an exception
}

int main() {
    try {
        riskyFunction();  // This might throw an exception
    } catch (const std::out_of_range& e) {
        std::cout << "Caught exception: " << e.what() << std::endl;  // Handling the exception
    }
    return 0;
}

Code breakdown:

  • try { riskyFunction(); : The try block contains the code that may throw an exception.
  • catch (const std::out_of_range& e): If an exception is thrown, it’s caught by the catch block. We handle the exception by printing the error message.

Why Use catch?

Without the catch block, an exception would propagate up the call stack until it either finds a handler or causes the program to terminate. By using catch, you ensure that the program gracefully handles errors without crashing. The catch block is your safety net.


Section 3: C++ Standard Exceptions

What Are Standard Exceptions?

C++ provides a set of standard exceptions that cover many common error conditions. These exceptions are built into the C++ Standard Library, which means you don’t need to write them yourself. Instead, you can use them right out of the box.

Some of the most commonly used standard exceptions are:

  • std::invalid_argument: Thrown when an invalid argument is passed to a function.
  • std::out_of_range: Thrown when an attempt is made to access an out-of-bounds element of a container (like an array or vector).
  • std::runtime_error: A generic exception thrown for runtime errors.

These exceptions provide clarity and help standardize error handling in your program.

Why Use Standard Exceptions?

The beauty of using standard exceptions is that they are already well-defined and easy to use. They’re familiar to other developers who may work with your code. By relying on the standard exceptions, you avoid reinventing the wheel and make your code easier to maintain and understand.

Let’s look at how to use a standard exception in C++:

#include <iostream>
#include <stdexcept>

void checkIndex(int index) {
    if (index < 0 || index > 10) {
        throw std::out_of_range("Index is out of range.");
    }
}

int main() {
    try {
        checkIndex(12);  // This will throw an exception
    } catch (const std::out_of_range& e) {
        std::cout << "Caught exception: " << e.what() << std::endl;  // Handling the exception
    }
    return 0;
}

When Not to Use Standard Exceptions

While standard exceptions are great for many situations, sometimes they don’t fit your needs perfectly. For example, if your application needs to throw an error with very specific details, it might be better to define a custom exception. But for most typical errors, standard exceptions are your best option.


Section 4: Defining Custom Exceptions

When Should You Define Your Own Exceptions?

Standard exceptions are powerful, but sometimes they don’t give you the flexibility you need. For instance, what if you're working on a custom file system and need to handle a specific error like file corruption? A generic std::runtime_error wouldn’t make sense in that context.

When you need to handle a situation that is unique to your program, it’s time to define custom exceptions. Custom exceptions allow you to define a specific error type that’s tailored to your application's needs.

How to Define Custom Exceptions in C++

To define a custom exception in C++, you need to create a class that inherits from std::exception and override the what() function to return an error message.

Here’s a simple example of defining a custom exception:

#include <iostream>
#include <stdexcept>

// Define custom exception class
class MyCustomException : public std::exception {
public:
    const char* what() const noexcept override {
        return "My custom exception occurred!";
    }
};

void triggerError(bool shouldThrow) {
    if (shouldThrow) {
        throw MyCustomException();  // Throwing the custom exception
    }
}

int main() {
    try {
        triggerError(true);  // This will throw our custom exception
    } catch (const MyCustomException& e) {
        std::cout << "Caught custom exception: " << e.what() << std::endl;
    }
    return 0;
}

Code breakdown:

  • class MyCustomException : public std::exception: This defines a new exception class.
  • const char* what() const noexcept override: This function returns a message that describes the error.

Why Create Custom Exceptions?

Creating custom exceptions allows you to tailor the error handling to your application's unique needs. Custom exceptions can carry more information than standard exceptions, which is helpful when you need to debug or log specific error conditions.


Conclusion

In this article, we’ve explored exception handling in C++ in depth. Here’s how everything ties together:

  1. Throwing Exceptions: Allows us to pass off errors to another part of the program that can handle them.
  2. Catching Exceptions: Gives us the ability to catch thrown exceptions and handle them gracefully.
  3. Standard Exceptions: Predefined exceptions that cover common error scenarios.
  4. Custom Exceptions: When standard exceptions don’t fit, we can define our own exceptions tailored to our specific needs.

By mastering exception handling, you gain control over how your program responds to unexpected situations. Rather than letting errors bring your program to a halt, you can gracefully recover from them, ensuring a smoother user experience.

So, next time you face a potential error in your code, ask yourself: Should I throw an exception? If you answer yes, then you’ll know exactly how to catch and handle that exception, making your code more resilient and reliable.