A Beginner’s Guide to Exception Handling Using Try-Catch

A Beginner’s Guide to Exception Handling Using Try-Catch

Exception handling is a powerful feature in C++ that provides a way to react to exceptional conditions (errors) that arise during the execution of a program. Unlike traditional error-handling techniques such as return codes, exception handling separates normal logic from error-handling logic, making programs easier to read and maintain.

What is an Exception?

An exception is an event that occurs during the execution of a program that disrupts the normal flow of instructions. This can happen for various reasons, such as invalid input, hardware failure, or logical errors like division by zero. In C++, exceptions represent such anomalous conditions that programs need to handle gracefully.

Advantages of Exception Handling Over Traditional Error Handling

C++ offers exception handling as an advantage over the C programming language, where errors are often managed by checking return codes. The traditional approach has several drawbacks:

  • It mixes error-checking code with regular program logic, reducing clarity.

  • It forces programmers to write repetitive conditional checks after each function call.

  • It complicates maintenance and debugging because error paths are scattered.

Exception handling solves these problems by providing a structured way to handle errors separately.

Types of Exceptions in C++

Exceptions can be broadly classified into two types based on when and how they occur:

Synchronous Exceptions

Synchronous exceptions occur as a direct result of the execution of specific instructions. Examples include accessing an invalid array index, dividing by zero, or dereferencing a null pointer. These exceptions happen predictably during program execution.

Asynchronous Exceptions

Asynchronous exceptions are events that occur independently of the current execution path. For example, hardware failures such as disk failures or external interrupts can cause asynchronous exceptions. These are less predictable and often harder to handle within software.

The Core Keywords: try, catch, and throw

C++ provides three specialized keywords to support exception handling: try, catch, and throw. These keywords enable the definition and management of exception-handling code blocks.

The try Block

The try keyword is used to define a block of code where exceptions might occur. The code inside this block is said to be «protected.» If an exception is thrown within this block, control is transferred to the appropriate catch block for handling.

The throw Keyword

When an error condition occurs, the program can signal it by «throwing» an exception. The throw keyword is used to raise an exception explicitly. This passes control to the nearest suitable catch block designed to handle that particular type of exception.

The catch Block

The catch block follows the try block and defines how to handle exceptions. Each catch block specifies a parameter that indicates the type of exception it can handle. When an exception is thrown, the program searches for the matching catch block to execute the appropriate error-handling code.

Why is Exception Handling Important

Exception handling plays a crucial role in developing robust and fault-tolerant software. It allows the program to deal with unexpected errors without crashing or producing incorrect results. Handling exceptions ensures:

  • The program can continue running or exit gracefully after encountering an error.

  • Separation of normal program logic from error handling improves code readability.

  • Developers can define custom error handling for different types of problems.

Basic Syntax of try-catch in C++

The fundamental structure of exception handling using try-catch looks like this:

csharp

CopyEdit

try {

    // Code that might throw an exception

    throw exception; // Throw an exception if an error occurs

}

catch (ExceptionType e) {

    // Code to handle the exception

}

The try block encloses the code where exceptions may arise. If an exception is thrown, the program looks for a catch block with a matching exception type. If found, it executes the handler code.

Using try-catch Blocks to Handle Exceptions in C++

Exception handling in C++ is implemented primarily using the try, throw, and catch keywords. Understanding how to properly use these keywords is essential for writing robust programs that can gracefully handle runtime errors.

How Try-Catch Works in Practice

When you run C++ code, various errors can occur due to programmer mistakes, invalid user input, or unexpected situations like insufficient memory or hardware failures. By default, many errors cause the program to terminate abruptly, often displaying an error message. Exception handling provides a structured mechanism to intercept these errors and respond appropriately without crashing.

A try block contains code that might throw exceptions. If everything goes smoothly, the program continues after the try-catch block. However, if an exception is thrown inside the try block, control immediately transfers to the matching catch block, where the exception is handled.

Anatomy of a try-catch Block

A typical try-catch block looks like this:

cpp

CopyEdit

try {

    // Code that may throw an exception

    throw ExceptionType(); // Throws an exception object

}

catch (ExceptionType& e) {

    // Code to handle the exception

}

The try keyword marks the start of the block where exceptions may be thrown. The throw statement raises an exception object or value. The catch block catches the exception of the matching type and executes its handler code.

Why Use try-catch Blocks?

The primary purpose of try-catch blocks is to separate normal code from error-handling code, which offers several advantages:

  • Readability: Error-handling logic does not clutter the main program flow.

  • Maintainability: Changes in error management do not affect the core logic.

  • Safety: Programs can recover or shut down gracefully in case of errors.

  • Modularity: Specific error handlers can be created for different exception types.

Examples of Exception Handling in C++

Let’s explore some practical examples to understand how try-catch works in C++.

Example 1: Accessing Vector Elements Safely

Vectors in C++ provide the at() method to access elements safely. If an invalid index is accessed, it throws an out-of-range exception.

cpp

CopyEdit

#include <iostream>

#include <vector>

#include <exception>

using namespace std;

int main() {

    vector<int> vec = {0, 1};

    try {

        cout << vec.at(2) << endl; // This will throw an exception because index 2 is out of range

    }

    catch (const out_of_range& e) {

        cout << «Exception occurred: » << e.what() << endl;

    }

    return 0;

}

Explanation

  • The vec. At (2), call attempts to access the third element, which does not exist.

  • Since at() throws an out_of_range exception when the index is invalid, control passes to the catch block.

  • The catch block receives the exception object by reference and uses the what() method to print the error message.

  • This prevents the program from crashing and provides a graceful error message.

Example 2: Division by Zero Exception

We can create a function that throws an exception when a division by zero is attempted.

cpp

CopyEdit

#include <iostream>

using namespace std;

double divide(int x, int y) {

    if (y == 0) {

        throw «Division by zero error»;

    }

    return static_cast<double>(x) / y;

}

int main() {

    int a = 15;

    int b = 0;

    try {

        double result = divide(a, b);

        cout << «Result: » << result << endl;

    }

    catch (const char* msg) {

        cerr << «Exception caught: » << msg << endl;

    }

    return 0;

}

Explanation

  • The divide function checks if the denominator is zero.

  • If zero, it throws a string literal indicating the error.

  • The main function wraps the call in a try block and catches the exception using catch(const char* msg).

  • This shows how you can throw and catch exceptions of different types (here, a C-style string).

Exception Handling with Multiple Catch Blocks

You can define multiple catch blocks to handle different exception types separately. This approach improves the granularity of error handling.

Example

cpp

CopyEdit

#include <iostream>

#include <vector>

#include <exception>

using namespace std;

int main() {

    vector<int> vec = {1, 2};

    try {

        int a = 10;

        int b = 0;

        if (b == 0) {

            throw runtime_error(«Division by zero»);

        }

        cout << «Division result: » << a / b << endl;

        cout << vec.at(5) << endl; // This will throw out_of_range exception

    }

    catch (const out_of_range& e) {

        cout << «Out of range exception: » << e.what() << endl;

    }

    catch (const runtime_error& e) {

        cout << «Runtime error: » << e.what() << endl;

    }

    catch (…) {

        cout << «Unknown exception caught» << endl;

    }

    return 0;

}

Explanation

  • The first catch block handles out-of-range exceptions from vector access.

  • The second catch block handles runtime_error exceptions such as division by zero.

  • The third catch block, using ellipsis (), catches any other exceptions that do not match previous handlers.

  • This structure ensures that exceptions are handled according to their types, providing specific error messages.

Understanding Exception Propagation

If an exception is not caught within the function where it is thrown, it propagates up the call stack until it is caught or causes the program to terminate. This allows higher-level functions to handle errors thrown in lower-level functions.

Example of Exception Propagation

cpp

CopyEdit

#include <iostream>

#include <exception>

using namespace std;

void funcB() {

    throw runtime_error(«Error in funcB»);

}

void funcA() {

    funcB(); // No try-catch here, so the exception propagates up

}

int main() {

    try {

        funcA();

    }

    catch (const runtime_error& e) {

        cout << «Caught exception: » << e.what() << endl;

    }

    return 0;

}

Explanation

  • funcB throws a runtime_error exception.

  • funcA calls funcB but does not catch the exception.

  • The exception propagates to the main, where it is caught in the try-catch block.

  • This propagation mechanism lets you handle exceptions at appropriate levels in the call hierarchy.

Standard Exceptions in C++

The C++ Standard Library defines several exception classes in the <exception> header to represent common error conditions. Using standard exceptions allows consistent error handling and better interoperability.

Common Standard Exceptions

  • std::exception — Base class for all standard exceptions.

  • std::logic_error — Indicates errors in program logic (e.g., domain errors).

  • std::runtime_error — Represents errors that can only be detected during runtime.

  • std::out_of_range — Thrown when attempting to access elements outside valid ranges.

  • std::invalid_argument — Thrown when invalid arguments are passed to functions.

  • std::bad_alloc — Thrown when memory allocation fails.

Example Using Standard Exceptions

cpp

CopyEdit

#include <iostream>

#include <stdexcept>

using namespace std;

int main() {

    try {

        throw invalid_argument(«Invalid argument provided»);

    }

    catch (const invalid_argument& e) {

        cout << «Caught invalid_argument exception: » << e.what() << endl;

    }

    catch (const exception& e) {

        cout << «Caught general exception: » << e.what() << endl;

    }

    return 0;

}

Explanation

  • The code throws an invalid_argument exception.

  • The catch block for invalid_argument executes first because it matches the exception type.

  • If the first catch block were not present, the generic exception catch block would handle it.

  • Using standard exceptions helps in writing clear, standardized exception handlers.

Best Practices for Exception Handling

Proper exception handling is key to building maintainable and reliable software. Here are some widely accepted best practices:

Use Exceptions for Exceptional Conditions

Exceptions should represent unexpected or exceptional situations, not normal program flow. Avoid using exceptions for common conditional checks.

Throw by Value, Catch by Reference

Throw exceptions as objects, not pointers or primitive types. Catch exceptions by reference to avoid slicing and unnecessary copying.

Provide Meaningful Exception Messages

When throwing exceptions, include descriptive messages or objects that help diagnose the problem.

Avoid Catch-All Unless Necessary

Use the catch-all (catch(…)) block sparingly, mainly for logging or cleanup before program termination. It should not be used for regular error handling.

Clean Up Resources with RAII

To avoid resource leaks during exceptions, use Resource Acquisition Is Initialization (RAII) techniques. This means managing resources with objects that automatically release resources in their destructors.

User-Defined Exceptions in C++

While C++ provides a rich set of standard exceptions, sometimes these are not enough to represent specific error conditions unique to your application. In such cases, defining custom exception classes is necessary.

Why Create User-Defined Exceptions?

  • To represent domain-specific errors that standard exceptions don’t capture.

  • To carry additional information beyond just an error message.

  • To improve error handling granularity and make debugging easier.

How to Define a User-Defined Exception Class

Typically, user-defined exceptions inherit from std::exception or one of its derived classes, so they integrate well with existing exception handling mechanisms.

Here is a simple example:

cpp

CopyEdit

#include <exception>  

#include <string>  

class MyException: public std::exception {  

private:  

    std::string message;  

Public:  

    explicit MyException(const std::string& msg) : message(msg) {}  

    const char* what() const noexcept override {  

        return message.c_str();  

    }  

};  

Explanation

  • MyException inherits from std::exception.

  • It stores a custom error message.

  • The what() method is overridden to return the error message. This method is used by catch blocks to retrieve the exception description.

  • The noexcept specifier indicates that what() won’t throw exceptions.

Throwing and Catching User-Defined Exceptions

cpp

CopyEdit

#include <iostream>  

int main() {  

    try {  

        throw MyException(«Something went wrong in my application»);  

    }  

    catch (const MyException& e) {  

        std::cout << «Caught MyException: » << e.what() << std::endl;  

    }  

    return 0;  

}  

Extending User-Defined Exceptions with Additional Data

You can add more data members to the exception class to provide richer context.

cpp

CopyEdit

class FileException: public std::exception {  

private:  

    std::string filename;  

    std::string message;  

pPublic  

    FileException(const std::string& file, const std::string& msg) : filename(file), message(msg) {}  

    const char* what() const noexcept override {  

        return message.c_str();  

    }  

    const std::string& getFileName() const noexcept {  

        return filename;  

    }  

};  

In the catch block, you can access both the message and the filename that caused the exception.

Exception Specifications (Deprecated)

Older C++ versions allowed you to specify which exceptions a function might throw using exception specifications. These are deprecated in C++11 and later, but understanding them is important for maintaining legacy code.

Syntax of Exception Specifications

cpp

CopyEdit

double myFunction() throw(int, std::bad_alloc);  

This means myFunction may throw exceptions of type int or std::bad_alloc. If it throws any other exception, std::unexpected() is called.

Dynamic Exception Specifications

  • Allowed listing specific exceptions.

  • If a function throws an exception not listed, std::unexpected() is called.

  • The empty throw specifier throw() meant the function does not throw any exceptions.

Why Were They Deprecated?

  • Poor usability: It was difficult to maintain accurate exception lists.

  • Unexpected behavior on violation: Calling std::unexpected() usually results in program termination.

  • C++11 introduced noexcept, which expresses whether a function can throw or not in a simpler way.

Modern Alternative: noexcept

cpp

CopyEdit

void foo() noexcept {  

    // This function promises not to throw exceptions  

}  

Functions marked noexcept will call std::terminate() if an exception escapes, providing a safer and more predictable contract than dynamic exception specifications.

Exception Safety Guarantees

When designing code that throws exceptions, understanding exception safety guarantees is crucial to prevent resource leaks, data corruption, or inconsistent program states.

The Three Levels of Exception Safety

Basic Guarantee — The program remains in a valid state after an exception. No resource leaks occur. Data may be modified, but it remains consistent.

Strong Guarantee — Operations are either completed successfully or have no effect (transactional). If an exception is thrown, the program state is rolled back to before the operation.

No-Throw Guarantee — Operations guarantee not to throw exceptions. Common for destructors or swap operations.

Example: Basic Guarantee

Suppose you insert an element into a container, but an exception occurs during insertion. Basic guarantee ensures the container remains in a valid state, but the insertion may be partial.

Example: Strong Guarantee

Using copy-and-swap idiom or transactional operations, if the insertion fails, the container remains exactly as before the operation started.

Stack Unwinding and RAII

When an exception is thrown, the  C++ runtime performs stack unwinding, which means destructors for all objects created since entering the try block are called in reverse order. This mechanism ensures proper cleanup of resources during exceptions.

Resource Acquisition Is Initialization (RAII)

RAII is a C++ idiom that ties resource management to object lifetime: allocate resource in the constructor, release resource in the destructor. By using RAII, you ensure that resources like memory, file handles, and locks are automatically cleaned up when exceptions occur.

Example: RAII Wrapper for a File Handle

cpp

CopyEdit

#include <cstdio>  

class File {  

    FILE* fp;  

public:  

    File(const char* filename) {  

        fp = fopen(filename, «r»);  

        if (!fp) throw std::runtime_error(«Failed to open file»);  

    }  

    ~File() {  

        if (fp) fclose(fp);  

    }  

    File(const File&) = delete;  

    File& operator=(const File&) = delete;  

 

    FILE* get() const { return fp; }  

};  

If an exception occurs, the destructor closes the file automatically, preventing leaks.

Throwing Exceptions in Constructors and Destructors

Constructors

Constructors can throw exceptions to indicate failure during object initialization.

cpp

CopyEdit

class Widget {  

public:  

    Widget() {  

        if (/* some error condition */) {  

            throw std::runtime_error(«Failed to construct Widget»);  

        }  

    }  

};  

If a constructor throws, the object is considered not fully constructed, and destructors for fully constructed members are called automatically.

Destructors

Destructors should never throw exceptions because they are called during stack unwinding. Throwing exceptions inside destructors leads to std::terminate() if another exception is already active. If a destructor must perform an operation that can fail, it should catch and handle exceptions internally, never letting them propagate.

Exception Handling in Inheritance Hierarchies

When dealing with polymorphic types, exception handling should also consider inheritance.

Catching Base vs Derived Exceptions

cpp

CopyEdit

class BaseException: public std::exception { };  

class DerivedException: public BaseException { };  

try {  

    throw DerivedException();  

}  

catch (BaseException& e) {  

    // This will catch DerivedException as well because of polymorphism  

}  

Using Polymorphic Exception Handling

By catching base class references, you can handle multiple derived exceptions uniformly.

Re-Throwing Exceptions

Sometimes you catch an exception to perform some action (like logging), but want the exception to propagate further. You can re-throw the current exception using throw without arguments inside a catch block.

cpp

CopyEdit

try {  

    // code that throws  

}  

catch (const std::exception& e) {  

    std::cerr << «Caught exception: » << e.what() << std::endl;  

    throw; // Re-throw the same exception  

}  

Customizing Exception Behavior with std::unexpected and std::terminate

When a function violates its exception specification, C++ calls std::unexpected(). The default behavior of std::unexpected() is to call std::terminate(), ending the program. You can customize these handlers:

cpp

CopyEdit

#include <exception>  

#include <iostream>  

void myUnexpected() {  

    std::cerr << «Unexpected exception caught!» << std::endl;  

    std::terminate();  

}  

int main() {  

    std::set_unexpected(myUnexpected);  

    // code with dynamic exception specification (deprecated)  

}  

Since dynamic exception specifications are deprecated, using noexcept and RAII patterns is preferred today.

Exception Handling Performance Considerations

Exception handling incurs some runtime cost, especially when exceptions are thrown and stack unwinding happens. However, modern compilers optimize try-catch mechanisms so that normal execution (when no exceptions occur) is efficient. Because exceptions should be reserved for exceptional conditions, their occasional overhead is justified by the benefits of cleaner error handling.

Guidelines for Exception-Safe Code

  • Prefer RAII to manage resources.

  • Write code with clear exception guarantees.

  • Avoid throwing exceptions from destructors.

  • Use noexcept to mark functions that do not throw.

  • Catch exceptions by reference, not by value or pointer.

  • Avoid exception specifications except for noexcept.

  • Clean up all acquired resources before propagating exceptions.

Advanced Exception Handling Techniques in C++

Exception Handling Best Practices in Large-Scale Applications

When working on large-scale software projects, robust exception handling is critical for stability and maintainability. Here are some best practices that experienced developers follow:

Centralized Exception Handling

Designate a central location, such as the main function or a dedicated error handler module, to catch and handle exceptions not caught elsewhere. This prevents uncaught exceptions from terminating the program unexpectedly. It also allows logging and graceful shutdown procedures.

Use Exception Hierarchies

Organize your exceptions into meaningful hierarchies to facilitate catching broad categories of errors or very specific issues. For example:

cpp

CopyEdit

class AppException: public std::exception {};  

class DatabaseException: public AppException {};  

class NetworkException public AppException {};  

This allows a catch block to handle all application exceptions or just specific ones as needed.

Avoid Catch-All Unless Necessary

Using catch(…) should be a last resort. It can obscure the actual error and make debugging difficult. When you do use it, ensure you log sufficient information or re-throw the exception.

Provide Meaningful Error Messages

Exceptions should carry informative messages or error codes to aid diagnosis. Avoid generic messages like «Error occurred.» Instead, describe what failed and possibly why.

Avoid Using Exceptions for Control Flow

Exceptions should be reserved for exceptional conditions, not regular control flow. Using exceptions to handle normal logic can degrade performance and reduce code clarity.

Exception Handling in Multithreaded Programs

With modern C++ supporting multithreading, handling exceptions in a multithreaded context requires careful design.

Exception Propagation Across Threads

Exceptions thrown in one thread do not propagate to other threads. Each thread must handle its exceptions independently. If an exception is not caught inside a thread, it calls std::terminate().

Strategies to Handle Exceptions in Threads

  • Catch inside the thread function: Wrap the thread’s work in a try-catch block and handle exceptions locally.

  • Store exceptions for later re-throwing: Use std::exception_ptr to capture exceptions and pass them back to the thread that spawned the worker. This can be combined with futures or promises.

Example of capturing exceptions in threads:

cpp

CopyEdit

#include <thread>  

#include <iostream>  

#include <exception>  

void threadFunction(std::exception_ptr& eptr) {  

    try {  

        throw std::runtime_error(«Error in thread»);  

    }  

    catch (…) {  

        eptr = std::current_exception();  

    }  

}  

int main() {  

    std::exception_ptr eptr;  

    std::thread t(threadFunction, std::ref(eptr));  

    t.join();  

    if (eptr) {  

        try {  

            std::rethrow_exception(eptr);  

        }  

        catch (const std::exception& e) {  

            std::cout << «Caught exception from thread: » << e.what() << std::endl;  

        }  

    }  

    return 0;  

}  

Using std::future and std::promise for Exception Handling

, std::async and futures automatically propagate exceptions thrown by the asynchronous function to the caller when calling. Get ().

Handling Exceptions in Constructors and Destructors Revisited

Constructors and destructors have unique roles in exception handling and require special attention.

Exceptions in Constructors

If a constructor throws an exception, the object’s destructor is not called because the object is considered not fully constructed. However, destructors of any fully constructed base classes and members will be called.

To manage resources safely during construction, use RAII wrappers for members that allocate resources. This ensures that partially constructed objects clean up properly.

Example:

cpp

CopyEdit

class Resource {  

public:  

    Resource() { /* allocate resource */ }  

    ~Resource() { /* release resource */ }  

};  

class Widget {  

    Resource res;  

public:  

    Widget() {  

        // If this throws, res’s destructor will be called automatically  

        throw std::runtime_error(«Constructor failed»);  

    }  

};  

Exceptions in Destructors

Destructors should never allow exceptions to propagate because:

  • Destructors are called during stack unwinding, so throwing exceptions leads to program termination.

  • If a destructor must call code that can throw, it should catch exceptions internally and handle or log them without re-throwing.

Example of a safe destructor:

cpp

CopyEdit

~MyClass() {  

    try {  

        // Cleanup code that might throw  

    }  

    catch (…) {  

        // Log error and suppress exception  

    }  

}  

Exception Safety in Move and Copy Operations

Move and copy constructors and assignment operators should also be exception-safe.

Strong Guarantee in Assignment Operators

The copy-and-swap idiom is a common way to provide a strong exception guarantee in assignment operators:

cpp

CopyEdit

class MyClass {  

public:  

    MyClass& operator=(MyClass other) {  

        swap(*this, other);  

        return *this;  

    }  

    friend void swap(MyClass& first, MyClass& second) noexcept {  

        // Swap internals  

    }  

};  

This idiom ensures that if an exception occurs during copying, the original object remains unchanged.

Move Constructors and noexcept

Move constructors should be marked noexcept whenever possible to enable optimizations such as in containers (e.g., std::vector uses move if it is noexcept).

Advanced Usage of throw, try, and catch

Throwing Arbitrary Types

While it is best practice to throw exceptions derived from std::exception, C++ allows throwing any type, including built-in types. However, this is discouraged because it complicates error handling and type matching in catch blocks.

Catching Multiple Exception Types

You can have multiple catch blocks to handle different exceptions separately:

cpp

CopyEdit

try {  

    // Code that may throw  

}  

catch (const std::invalid_argument& e) {  

    // Handle invalid argument  

}  

catch (const std::out_of_range& e) {  

    // Handle out-of-range  

}  

catch (…) {  

    // Catch all other exceptions  

}  

Nested try-catch Blocks

You can nest try-catch blocks to handle exceptions at different granularities:

cpp

CopyEdit

try {  

    try {  

        // Code that may throw  

    }  

    catch (const std::logic_error& e) {  

        // Handle logic errors  

        throw;  // Re-throw for outer catch  

    }  

}  

catch (const std::exception& e) {  

    // Handle all exceptions derived from std::exception  

}  

Exception Handling and Standard Library Integration

std::exception and Derived Classes

C++ Standard Library defines many exception types derived from std::exception, including:

  • std::logic_error (e.g., std::invalid_argument, std::domain_error)

  • std::runtime_error (e.g., std::range_error, std::overflow_error)

  • std::bad_alloc (memory allocation failure)

  • std::out_of_range (invalid container access)

Using these classes improves compatibility and allows you to catch categories of errors easily.

Exception Guarantees in STL Containers

Most STL containers provide strong exception safety guarantees, meaning operations either complete successfully or leave the container unchanged. This is possible because they use noexcept move constructors and strong exception-safe implementations internally.

Debugging Exceptions in C++

Using Exception Diagnostics

When debugging, knowing where an exception was thrown helps in diagnosis. Tools and techniques include:

  • Enabling core dumps or crash dumps.

  • Using debuggers that break on throw statements (e.g., gdb with catch throw).

  • Adding detailed logging in catch blocks.

Handling Unknown Exceptions

Using catch(…) allows capturing any exception type, but you cannot inspect the exception object itself. This is useful as a last-resort safety net. Combine it with std::current_exception() and std::rethrow_exception() to attempt further handling.

Writing Custom Exception Classes with Rich Context

Adding Contextual Information

Sometimes exceptions need more context than a message. You can add members such as error codes, source file names, line numbers, or additional metadata.

Example:

cpp

CopyEdit

class DetailedException: public std::exception {  

    int errorCode;  

    std::string file;  

    int line;  

    std::string message;  

public:  

    DetailedException(int code, const std::string& file, int line, const std::string& msg)   

        : errorCode(code), file(file), line(line), message(msg) {}  

    const char* what() const noexcept override {  

        return message.c_str();  

    }  

    int getCode() const noexcept { return errorCode; }  

    const std::string& getFile() const noexcept { return file; }  

    int getLine() const noexcept { return line; }  

};  

Macros to Automate File and Line Information

cpp

CopyEdit

#define THROW_DETAILED_EXCEPTION(code, msg) \  

    throw DetailedException(code, __FILE__, __LINE__, msg)  

This macro simplifies throwing exceptions with the source location automatically.

Exception Handling in Modern C++ Features

Exceptions and Lambdas

You can throw exceptions inside lambda expressions and catch them outside:

cpp

CopyEdit

auto lambda = []() {  

    throw std::runtime_error(«Error in lambda»);  

};  

try {  

    lambda();  

}  

catch (const std::exception& e) {  

    std::cout << e.what() << std::endl;  

}  

Exceptions in Template Code

Templates that use operations that might throw must either propagate exceptions or catch and handle them internally. Exception handling in templates is similar to normal code but requires attention to type safety and exception specifications.

When Not to Use Exceptions

Embedded Systems and Performance-Critical Code

In systems with limited resources or hard real-time requirements, exceptions might be disabled or avoided due to overhead and unpredictability. In such cases, error codes or other mechanisms are preferred.

Interfacing with C Code

When integrating C++ code with C libraries, error handling often uses return codes. Care must be taken to convert errors into exceptions safely if desired.