Progress10 of 20 topics

50% complete

Exception Handling in C++

Exception handling is a powerful mechanism in C++ that helps you manage runtime errors in a controlled manner. It allows your program to detect, propagate, and handle errors efficiently, making your applications more robust and reliable.

What You'll Learn

  • Understanding exceptions and why they're important
  • Using try-catch blocks to handle exceptions
  • Throwing exceptions with the throw keyword
  • Creating and using custom exception classes
  • Exception specifications and noexcept
  • Standard exception classes in C++
  • Best practices for effective exception handling

What Are Exceptions?

An exception is an unexpected problem that arises during program execution. Unlike regular error-handling techniques (such as error codes), exceptions provide a structured and type-safe way to separate error detection from error handling, making code cleaner and more maintainable.

Traditional Error Handling

cpp
// Using error codes
int divide(int a, int b, int* result) {
if (b == 0) {
return -1; // Error code for division by zero
}
*result = a / b;
return 0; // Success code
}
// Usage
int main() {
int result;
int status = divide(10, 0, &result);
if (status == -1) {
// Handle error
std::cout << "Error: Division by zero\n";
} else {
std::cout << "Result: " << result << "\n";
}
return 0;
}

Exception-Based Handling

cpp
// Using exceptions
int divide(int a, int b) {
if (b == 0) {
throw std::runtime_error("Division by zero");
}
return a / b;
}
// Usage
int main() {
try {
int result = divide(10, 0);
std::cout << "Result: " << result << "\n";
} catch (const std::runtime_error& e) {
// Handle exception
std::cout << "Error: " << e.what() << "\n";
}
return 0;
}

The try-catch Block

The try-catch block is the fundamental mechanism for handling exceptions in C++. Code that might throw an exception is placed in a try block, and potential exceptions are caught and handled in one or more catch blocks.

cpp
1#include <iostream>
2#include <stdexcept>
3
4int main() {
5 try {
6 // Code that might throw an exception
7 std::cout << "Attempting to perform an operation..." << std::endl;
8
9 // Simulate an error
10 throw std::runtime_error("Something went wrong!");
11
12 // This code will not be executed
13 std::cout << "This line will not be reached." << std::endl;
14 }
15 catch (const std::runtime_error& e) {
16 // Handle std::runtime_error
17 std::cout << "Caught a runtime_error: " << e.what() << std::endl;
18 }
19 catch (const std::exception& e) {
20 // Handle any exception derived from std::exception
21 std::cout << "Caught an exception: " << e.what() << std::endl;
22 }
23 catch (...) {
24 // Catch-all handler for any other exceptions
25 std::cout << "Caught an unknown exception!" << std::endl;
26 }
27
28 // Program continues after exception handling
29 std::cout << "Program continues after exception handling." << std::endl;
30
31 return 0;
32}

Structure of try-catch Blocks

  • try block: Contains code that might throw exceptions
  • catch blocks: Handle specific types of exceptions
  • catch(...): Catches any exception (catch-all handler)
  • Multiple catch blocks are evaluated in order (from most specific to most general)
  • Only the first matching catch block executes

Throwing Exceptions

In C++, you can throw exceptions using the throw keyword. Anything can be thrown as an exception, though it's best practice to throw objects derived from std::exception.

cpp
1#include <iostream>
2#include <stdexcept>
3#include <string>
4
5// Function that might throw an exception
6double divide(double numerator, double denominator) {
7 if (denominator == 0) {
8 throw std::invalid_argument("Denominator cannot be zero");
9 }
10 return numerator / denominator;
11}
12
13// Function with different types of exceptions
14void processValue(int value) {
15 if (value < 0) {
16 throw std::out_of_range("Value cannot be negative");
17 }
18 if (value == 0) {
19 throw std::invalid_argument("Value cannot be zero");
20 }
21 if (value > 1000) {
22 throw std::runtime_error("Value too large to process");
23 }
24
25 // Process the valid value
26 std::cout << "Processing value: " << value << std::endl;
27}
28
29int main() {
30 try {
31 // Test the divide function
32 std::cout << "Result: " << divide(10, 0) << std::endl;
33 }
34 catch (const std::invalid_argument& e) {
35 std::cout << "Invalid argument: " << e.what() << std::endl;
36 }
37
38 // Try different values
39 int testValues[] = {-5, 0, 50, 2000};
40 for (int val : testValues) {
41 try {
42 std::cout << "Testing value " << val << "... ";
43 processValue(val);
44 }
45 catch (const std::exception& e) {
46 std::cout << "Error: " << e.what() << std::endl;
47 }
48 }
49
50 return 0;
51}

What Can Be Thrown?

C++ allows you to throw:

  • Objects of any type (including built-in types)
  • Pointers
  • References
  • Temporaries

However, best practice is to throw objects of types derived from std::exception.

Creating Custom Exception Classes

While C++ provides standard exception classes, you can create your own exception classes for application-specific errors. Deriving from std::exceptionensures compatibility with existing exception handling code.

cpp
1#include <iostream>
2#include <stdexcept>
3#include <string>
4
5// Custom exception class derived from std::exception
6class DatabaseException : public std::exception {
7private:
8 std::string m_message;
9 int m_errorCode;
10
11public:
12 DatabaseException(const std::string& message, int errorCode)
13 : m_message(message), m_errorCode(errorCode) {}
14
15 // Override what() to provide error message
16 const char* what() const noexcept override {
17 return m_message.c_str();
18 }
19
20 // Additional method to get error code
21 int getErrorCode() const {
22 return m_errorCode;
23 }
24};
25
26// More specific database exceptions
27class ConnectionException : public DatabaseException {
28public:
29 ConnectionException(const std::string& message, int errorCode)
30 : DatabaseException("Connection error: " + message, errorCode) {}
31};
32
33class QueryException : public DatabaseException {
34public:
35 QueryException(const std::string& message, int errorCode)
36 : DatabaseException("Query error: " + message, errorCode) {}
37};
38
39// Simulate database operations
40void connectToDatabase(const std::string& server) {
41 if (server.empty()) {
42 throw ConnectionException("Empty server name", 1001);
43 }
44
45 if (server == "down") {
46 throw ConnectionException("Server is down", 1002);
47 }
48
49 std::cout << "Connected to " << server << std::endl;
50}
51
52void executeQuery(const std::string& query) {
53 if (query.empty()) {
54 throw QueryException("Empty query", 2001);
55 }
56
57 if (query == "DELETE") {
58 throw QueryException("Dangerous query without WHERE clause", 2002);
59 }
60
61 std::cout << "Executing query: " << query << std::endl;
62}
63
64int main() {
65 try {
66 // Try to connect to database
67 connectToDatabase("down");
68
69 // Try to execute a query
70 executeQuery("SELECT * FROM users");
71 }
72 catch (const ConnectionException& e) {
73 std::cout << e.what() << " (Error code: " << e.getErrorCode() << ")" << std::endl;
74 }
75 catch (const QueryException& e) {
76 std::cout << e.what() << " (Error code: " << e.getErrorCode() << ")" << std::endl;
77 }
78 catch (const DatabaseException& e) {
79 std::cout << "Database error: " << e.what() << std::endl;
80 }
81 catch (const std::exception& e) {
82 std::cout << "Standard exception: " << e.what() << std::endl;
83 }
84
85 return 0;
86}

Best Practices for Custom Exceptions

  • Derive from std::exception or its derived classes
  • Override the what() method to provide meaningful error messages
  • Mark what() as noexcept to prevent exceptions from escaping
  • Create a hierarchy of exception classes for different error categories
  • Include relevant error information (like error codes, context, etc.)

Standard Exception Classes

C++ provides a hierarchy of standard exception classes in the <stdexcept>,<exception>, <new>, and other headers. These cover common error scenarios and should be used when appropriate.

Exception ClassHeaderDescription
std::exception<exception>Base class for all standard exceptions
std::logic_error<stdexcept>Errors that could be detected at compile time
std::invalid_argument<stdexcept>Invalid arguments to functions
std::domain_error<stdexcept>Domain errors in mathematical functions
std::length_error<stdexcept>Attempt to exceed maximum allowed size
std::out_of_range<stdexcept>Out-of-range array or container access
std::runtime_error<stdexcept>Errors detected only during runtime
std::overflow_error<stdexcept>Arithmetic overflow errors
std::underflow_error<stdexcept>Arithmetic underflow errors
std::system_error<system_error>Errors from the operating system or other low-level APIs
std::bad_alloc<new>Failed memory allocation with new
cpp
1#include <iostream>
2#include <stdexcept>
3#include <vector>
4#include <string>
5#include <new>
6
7int main() {
8 // std::out_of_range
9 try {
10 std::vector<int> numbers = {1, 2, 3};
11 std::cout << "Accessing element 5: " << numbers.at(5) << std::endl;
12 }
13 catch (const std::out_of_range& e) {
14 std::cout << "Out of range error: " << e.what() << std::endl;
15 }
16
17 // std::invalid_argument
18 try {
19 int value = std::stoi("not a number");
20 }
21 catch (const std::invalid_argument& e) {
22 std::cout << "Invalid argument error: " << e.what() << std::endl;
23 }
24
25 // std::bad_alloc
26 try {
27 // Try to allocate a very large amount of memory
28 int* hugeArray = new int[1000000000];
29 delete[] hugeArray;
30 }
31 catch (const std::bad_alloc& e) {
32 std::cout << "Memory allocation error: " << e.what() << std::endl;
33 }
34
35 return 0;
36}

Exception Specifications and noexcept

C++ allows you to specify which exceptions a function might throw using exception specifications. Modern C++ encourages the use of noexcept to indicate functions that don't throw exceptions.

Note on Dynamic Exception Specifications

Dynamic exception specifications (throw(type1, type2)) are deprecated in C++11 and removed in C++17. Use noexcept instead for modern C++ code.

cpp
1#include <iostream>
2#include <stdexcept>
3
4// Function that doesn't throw exceptions
5void safeFunction() noexcept {
6 std::cout << "This function is guaranteed not to throw exceptions" << std::endl;
7}
8
9// Function that might throw exceptions
10void riskyFunction() {
11 if (rand() % 2 == 0) {
12 throw std::runtime_error("Something went wrong");
13 }
14 std::cout << "Function completed successfully" << std::endl;
15}
16
17// Function with conditional noexcept
18template <typename T>
19T add(T a, T b) noexcept(noexcept(a + b)) {
20 return a + b;
21}
22
23// Checking if a function is noexcept
24template <typename T>
25void checkNoexcept() {
26 if (noexcept(T())) {
27 std::cout << "Default constructor is noexcept" << std::endl;
28 } else {
29 std::cout << "Default constructor may throw exceptions" << std::endl;
30 }
31}
32
33int main() {
34 // Using noexcept functions
35 safeFunction();
36
37 // Using potentially throwing functions
38 try {
39 riskyFunction();
40 }
41 catch (const std::exception& e) {
42 std::cout << "Caught exception: " << e.what() << std::endl;
43 }
44
45 // Checking noexcept with different types
46 checkNoexcept<int>();
47 checkNoexcept<std::string>();
48
49 return 0;
50}

Function Try Blocks

Function try blocks allow you to catch exceptions thrown during initialization of member variables in constructors or during the execution of a function.

cpp
1#include <iostream>
2#include <stdexcept>
3#include <string>
4
5class Resource {
6private:
7 std::string m_name;
8
9public:
10 Resource(const std::string& name) : m_name(name) {
11 if (name.empty()) {
12 throw std::invalid_argument("Resource name cannot be empty");
13 }
14 std::cout << "Resource '" << m_name << "' created" << std::endl;
15 }
16
17 ~Resource() {
18 std::cout << "Resource '" << m_name << "' destroyed" << std::endl;
19 }
20};
21
22class Application {
23private:
24 Resource m_resource1;
25 Resource m_resource2;
26
27public:
28 // Function try block for constructor
29 Application(const std::string& name1, const std::string& name2)
30 try : m_resource1(name1), m_resource2(name2) {
31 std::cout << "Application initialized successfully" << std::endl;
32 }
33 catch (const std::exception& e) {
34 std::cout << "Exception during Application initialization: " << e.what() << std::endl;
35 // Re-throw the exception
36 throw;
37 }
38
39 // Function try block for a regular method
40 void run() try {
41 std::cout << "Application running..." << std::endl;
42 // Potentially throwing code
43 if (rand() % 2 == 0) {
44 throw std::runtime_error("Application encountered an error");
45 }
46 std::cout << "Application completed successfully" << std::endl;
47 }
48 catch (const std::exception& e) {
49 std::cout << "Exception during run: " << e.what() << std::endl;
50 // Handle or re-throw
51 }
52};
53
54int main() {
55 try {
56 // This will succeed
57 Application app1("Database", "Network");
58 app1.run();
59
60 // This will fail (empty resource name)
61 Application app2("Logger", "");
62 }
63 catch (const std::exception& e) {
64 std::cout << "Main caught: " << e.what() << std::endl;
65 }
66
67 return 0;
68}

Stack Unwinding and Resource Management

When an exception is thrown, C++ performs "stack unwinding," which means it unwinds the stack frame by frame, calling destructors for all automatic objects. This is why RAII (Resource Acquisition Is Initialization) is important for exception-safe code.

cpp
1#include <iostream>
2#include <stdexcept>
3#include <memory>
4#include <vector>
5
6class Resource {
7private:
8 std::string m_name;
9
10public:
11 Resource(const std::string& name) : m_name(name) {
12 std::cout << "Resource '" << m_name << "' acquired" << std::endl;
13 }
14
15 ~Resource() {
16 std::cout << "Resource '" << m_name << "' released" << std::endl;
17 }
18
19 void use() {
20 std::cout << "Using resource '" << m_name << "'" << std::endl;
21 }
22};
23
24// Poor exception safety - resources may leak
25void unsafeFunction() {
26 Resource* r1 = new Resource("Memory");
27 Resource* r2 = new Resource("Database");
28
29 // If an exception is thrown here, r1 and r2 will leak
30 throw std::runtime_error("Something went wrong");
31
32 // This code is never reached
33 delete r2;
34 delete r1;
35}
36
37// Better exception safety with RAII and smart pointers
38void safeFunction() {
39 // Resources automatically managed
40 std::unique_ptr<Resource> r1 = std::make_unique<Resource>("Memory");
41 auto r2 = std::make_unique<Resource>("Database");
42
43 // Even if an exception is thrown, resources will be properly released
44 throw std::runtime_error("Something went wrong");
45
46 // No need to manually release resources
47}
48
49int main() {
50 // Test unsafe function
51 try {
52 std::cout << "Calling unsafeFunction()..." << std::endl;
53 unsafeFunction();
54 }
55 catch (const std::exception& e) {
56 std::cout << "Exception caught: " << e.what() << std::endl;
57 std::cout << "Note: Resources leaked!" << std::endl;
58 }
59
60 std::cout << "\n------------------------\n" << std::endl;
61
62 // Test safe function
63 try {
64 std::cout << "Calling safeFunction()..." << std::endl;
65 safeFunction();
66 }
67 catch (const std::exception& e) {
68 std::cout << "Exception caught: " << e.what() << std::endl;
69 std::cout << "Note: All resources properly released" << std::endl;
70 }
71
72 return 0;
73}

Exception Handling Best Practices

Do

  • Use exceptions for exceptional conditions, not for normal control flow
  • Throw by value, catch by const reference
  • Derive custom exceptions from std::exception
  • Implement the what() method for meaningful error messages
  • Use RAII to manage resources and ensure proper cleanup
  • Keep try blocks as small as possible
  • Document which exceptions your functions might throw
  • Use noexcept for functions that won't throw

Don't

  • Don't throw exceptions from destructors
  • Don't use exceptions for expected error conditions
  • Don't catch exceptions you can't handle properly
  • Don't use empty catch blocks (swallowing exceptions)
  • Don't leak resources when exceptions occur
  • Don't throw exceptions across DLL boundaries
  • Don't throw built-in types (like int or char*)
  • Don't use dynamic exception specifications (deprecated)
cpp
1#include <iostream>
2#include <stdexcept>
3#include <fstream>
4#include <string>
5#include <memory>
6
7// Good: Custom exception derived from std::exception
8class FileError : public std::runtime_error {
9public:
10 FileError(const std::string& filename, const std::string& message)
11 : std::runtime_error("File '" + filename + "': " + message),
12 m_filename(filename) {}
13
14 const std::string& getFilename() const { return m_filename; }
15
16private:
17 std::string m_filename;
18};
19
20// Good: Using RAII for resource management
21class FileReader {
22private:
23 std::ifstream m_file;
24 std::string m_filename;
25
26public:
27 FileReader(const std::string& filename) : m_filename(filename) {
28 m_file.open(filename);
29 if (!m_file) {
30 throw FileError(filename, "Could not open file");
31 }
32 }
33
34 // No need for explicit close - RAII handles it in destructor
35 ~FileReader() {
36 if (m_file.is_open()) {
37 m_file.close();
38 }
39 }
40
41 std::string readLine() {
42 std::string line;
43 if (std::getline(m_file, line)) {
44 return line;
45 } else if (m_file.eof()) {
46 throw FileError(m_filename, "End of file reached");
47 } else {
48 throw FileError(m_filename, "Error reading from file");
49 }
50 }
51};
52
53// Good: Functions clearly document exception behavior
54void processFile(const std::string& filename) {
55 try {
56 // Small, focused try block
57 FileReader reader(filename);
58
59 // Process the file
60 std::string line = reader.readLine();
61 std::cout << "First line: " << line << std::endl;
62 }
63 // Good: Catching by const reference
64 catch (const FileError& e) {
65 std::cout << "File error: " << e.what() << std::endl;
66
67 // Log the error, attempt recovery, or rethrow
68 }
69 catch (const std::exception& e) {
70 std::cout << "Unexpected error: " << e.what() << std::endl;
71 throw; // Rethrow to let caller handle it
72 }
73}
74
75int main() {
76 try {
77 processFile("nonexistent.txt");
78 }
79 catch (const std::exception& e) {
80 std::cout << "Error in main: " << e.what() << std::endl;
81 }
82
83 return 0;
84}

Summary

Exception handling in C++ provides a powerful mechanism for managing errors in a structured way. By separating error detection from error handling, exceptions help make code more readable, maintainable, and robust.

  • Use try-catch blocks to handle exceptions
  • Throw exceptions using the throw keyword
  • Create custom exception classes by deriving from std::exception
  • Use standard exception classes for common error scenarios
  • Mark functions that don't throw with noexcept
  • Use RAII to ensure resources are properly managed even when exceptions occur
  • Follow best practices to write exception-safe code

By understanding and properly implementing exception handling in your C++ applications, you can create more robust, maintainable, and user-friendly software.

Related Tutorials

Learn how to work with files in C++.

Learn more

Understanding memory allocation and deallocation in C++.

Learn more

Explore object-oriented programming principles in C++.

Learn more