In the previous lesson, you learned why return codes create problems by mixing error handling with normal control flow. C++ exceptions solve this by separating error signaling from error handling using three keywords: throw, try, and catch.

Throwing exceptions

Think of exceptions like alert signals. When a smoke detector senses smoke, it emits a loud alarm. Similarly, when code encounters an error, it throws an exception to alert the program that something went wrong.

In C++, a throw statement signals that an exception has occurred. The term "raising an exception" is also commonly used.

You can throw any value:

throw 404;                                    // Throw an integer
throw "Connection failed";                     // Throw a C-style string
throw std::string{"Invalid configuration"};    // Throw a std::string
throw 3.14;                                   // Throw a double
throw NetworkError{"Timeout"};                // Throw a custom class

Each throw statement signals a problem that needs handling. The thrown value typically describes what went wrong.

Detecting exceptions with try blocks

When a smoke alarm goes off, people notice and react. In C++, a try block acts as an observer, watching for exceptions thrown by code inside it.

Here's a try block:

try
{
    // Code that might throw exceptions
    throw "Network error";
}

The try block doesn't specify how to handle exceptions - it just watches for them. If any code inside throws an exception, the try block catches it and looks for a matching handler.

Handling exceptions with catch blocks

When a smoke alarm sounds, you take action - investigate, evacuate, or call emergency services. A catch block defines what action to take when an exception occurs.

Here's a catch block that handles string exceptions:

catch (const char* message)
{
    std::cerr << "Error: " << message << '\n';
}

Try and catch blocks work together. The try block detects exceptions; the catch block handles them. A try block must have at least one catch block immediately following it, but can have multiple catches for different exception types.

Once a catch block handles an exception, execution continues normally with the first statement after all catch blocks.

Complete example with try, throw, and catch

Here's a full program demonstrating exception handling:

#include <iostream>

int main()
{
    try
    {
        std::cout << "Connecting to server...\n";
        throw "Connection timeout";
        std::cout << "Connected successfully\n";  // Never executes
    }
    catch (const char* error)
    {
        std::cerr << "Network error: " << error << '\n';
    }
    catch (int errorCode)
    {
        std::cerr << "Error code: " << errorCode << '\n';
    }

    std::cout << "Program continues\n";

    return 0;
}

Output:

Connecting to server...
Network error: Connection timeout
Program continues

The throw statement causes immediate transfer to the matching catch block. The "Connected successfully" message never prints because execution jumps directly to exception handling.

Catch block parameters

Catch blocks work like function parameters. The parameter receives the thrown value:

catch (int errorCode)
{
    std::cout << "Received error code: " << errorCode << '\n';
}

For fundamental types, catch by value. For class types, catch by const reference to avoid copying:

catch (const std::string& message)  // Catch by const reference
{
    std::cerr << "Error: " << message << '\n';
}

If you don't need the exception value, omit the parameter name:

catch (int)  // No parameter name
{
    std::cerr << "An integer exception occurred\n";
}

This prevents unused variable warnings.

Multiple catch blocks

A single try block can have multiple catch blocks for different exception types:

#include <iostream>
#include <string>

int main()
{
    try
    {
        throw 500;  // Throw an integer
    }
    catch (const std::string& error)
    {
        std::cerr << "String exception: " << error << '\n';
    }
    catch (int code)
    {
        std::cerr << "Integer exception: " << code << '\n';
    }
    catch (double value)
    {
        std::cerr << "Double exception: " << value << '\n';
    }

    std::cout << "Continuing\n";

    return 0;
}

Output:

Integer exception: 500
Continuing

The thrown exception is routed to the first matching catch block. Once handled, execution continues after all catch blocks.

No type conversion for exceptions

Unlike function arguments, exceptions don't undergo implicit type conversion:

try
{
    throw 3.14;  // Throws a double
}
catch (int value)  // Won't match - int doesn't convert from double
{
    std::cout << "Caught int: " << value << '\n';
}

This won't compile because the double exception doesn't match the int catch block. However, inheritance-based conversions (derived-to-base) do work.

A practical example: Configuration validation

Here's a realistic use of exceptions:

#include <iostream>
#include <string>

void validatePort(int port)
{
    if (port < 1 || port > 65535)
        throw "Port number out of valid range (1-65535)";
}

int main()
{
    std::cout << "Enter port number: ";
    int port{};
    std::cin >> port;

    try
    {
        validatePort(port);
        std::cout << "Valid port: " << port << '\n';
        std::cout << "Connecting...\n";
    }
    catch (const char* error)
    {
        std::cerr << "Configuration error: " << error << '\n';
    }

    return 0;
}

Valid input:

Enter port number: 8080
Valid port: 8080
Connecting...

Invalid input:

Enter port number: 99999
Configuration error: Port number out of valid range (1-65535)

The exception separates validation logic from normal flow. If validation fails, an exception bypasses the connection code and jumps to error handling.

Exceptions are handled immediately

When an exception is thrown, execution immediately transfers to the nearest try block's catch handlers:

#include <iostream>

int main()
{
    try
    {
        std::cout << "Starting\n";
        throw 42;
        std::cout << "After throw\n";  // Never executes
    }
    catch (int value)
    {
        std::cout << "Caught: " << value << '\n';
    }

    std::cout << "Done\n";

    return 0;
}

Output:

Starting
Caught: 42
Done

The code after throw never runs. Execution jumps immediately to the catch block.

Common catch block patterns

Catch blocks typically perform one of these actions:

1. Log and continue

catch (const char* error)
{
    std::cerr << "Warning: " << error << '\n';
    // Program continues
}

2. Return error code to caller

bool connectToServer()
{
    try
    {
        // Connection code that might throw
        return true;
    }
    catch (...)
    {
        return false;
    }
}

3. Rethrow a different exception

catch (const char* lowLevelError)
{
    std::cerr << "Low-level error: " << lowLevelError << '\n';
    throw std::string{"High-level connection failed"};
}

4. Terminate gracefully

int main()
{
    try
    {
        // Application code
    }
    catch (...)
    {
        std::cerr << "Fatal error, shutting down\n";
        return 1;
    }

    return 0;
}

A more complex example: Resource initialization

Here's how exceptions simplify error handling when multiple operations can fail:

#include <iostream>
#include <string>

class ServerConfig
{
private:
    std::string m_host;
    int m_port;
    int m_timeout;

public:
    ServerConfig(const std::string& host, int port, int timeout)
        : m_host{host}, m_port{port}, m_timeout{timeout}
    {
        if (m_host.empty())
            throw "Host cannot be empty";

        if (m_port < 1 || m_port > 65535)
            throw "Invalid port number";

        if (m_timeout < 0)
            throw "Timeout cannot be negative";
    }

    void display() const
    {
        std::cout << "Host: " << m_host << '\n';
        std::cout << "Port: " << m_port << '\n';
        std::cout << "Timeout: " << m_timeout << "s\n";
    }
};

int main()
{
    try
    {
        ServerConfig config{"localhost", 8080, 30};
        config.display();

        std::cout << "\nTrying invalid config...\n";
        ServerConfig badConfig{"", 8080, 30};  // Throws exception
        badConfig.display();  // Never executes
    }
    catch (const char* error)
    {
        std::cerr << "Configuration error: " << error << '\n';
    }

    std::cout << "Program continues\n";

    return 0;
}

Output:

Host: localhost
Port: 8080
Timeout: 30s

Trying invalid config...
Configuration error: Host cannot be empty
Program continues

The constructor validates parameters and throws if they're invalid. This prevents creating misconfigured objects.

Understanding exception flow

Exception handling follows this process:

  1. Code throws an exception using throw
  2. Execution immediately stops and searches for the nearest enclosing try block
  3. If found, the try block checks its catch handlers in order
  4. The first matching catch handler executes
  5. After the catch block completes, execution continues normally after all catch blocks
  6. If no matching catch is found, the search continues up the call stack
  7. If no handler is found anywhere, the program terminates

Here's a simple summary: throw signals a problem, try watches for problems, and catch handles them. After handling, normal execution resumes.

Recommendations

When using basic exception handling:

  • Catch fundamental types by value
  • Catch class types by const reference
  • Use specific exception types when possible
  • Keep catch blocks focused and simple
  • Don't throw exceptions for normal control flow
  • Document which exceptions your functions might throw

Summary

Exception handling in C++ uses three keywords:

  • throw: Signals that an error has occurred
  • try: Monitors code for exceptions
  • catch: Handles specific exception types

This mechanism separates error signaling from error handling, making code cleaner and more maintainable than return-code-based approaches. In upcoming lessons, you'll learn about exception propagation, custom exception classes, and best practices for robust exception handling.