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
// Using error codesint 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}// Usageint main() {int result;int status = divide(10, 0, &result);if (status == -1) {// Handle errorstd::cout << "Error: Division by zero\n";} else {std::cout << "Result: " << result << "\n";}return 0;}
Exception-Based Handling
// Using exceptionsint divide(int a, int b) {if (b == 0) {throw std::runtime_error("Division by zero");}return a / b;}// Usageint main() {try {int result = divide(10, 0);std::cout << "Result: " << result << "\n";} catch (const std::runtime_error& e) {// Handle exceptionstd::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.
1#include <iostream>2#include <stdexcept>34int main() {5 try {6 // Code that might throw an exception7 std::cout << "Attempting to perform an operation..." << std::endl;89 // Simulate an error10 throw std::runtime_error("Something went wrong!");1112 // This code will not be executed13 std::cout << "This line will not be reached." << std::endl;14 }15 catch (const std::runtime_error& e) {16 // Handle std::runtime_error17 std::cout << "Caught a runtime_error: " << e.what() << std::endl;18 }19 catch (const std::exception& e) {20 // Handle any exception derived from std::exception21 std::cout << "Caught an exception: " << e.what() << std::endl;22 }23 catch (...) {24 // Catch-all handler for any other exceptions25 std::cout << "Caught an unknown exception!" << std::endl;26 }2728 // Program continues after exception handling29 std::cout << "Program continues after exception handling." << std::endl;3031 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
.
1#include <iostream>2#include <stdexcept>3#include <string>45// Function that might throw an exception6double divide(double numerator, double denominator) {7 if (denominator == 0) {8 throw std::invalid_argument("Denominator cannot be zero");9 }10 return numerator / denominator;11}1213// Function with different types of exceptions14void 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 }2425 // Process the valid value26 std::cout << "Processing value: " << value << std::endl;27}2829int main() {30 try {31 // Test the divide function32 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 }3738 // Try different values39 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 }4950 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::exception
ensures compatibility with existing exception handling code.
1#include <iostream>2#include <stdexcept>3#include <string>45// Custom exception class derived from std::exception6class DatabaseException : public std::exception {7private:8 std::string m_message;9 int m_errorCode;1011public:12 DatabaseException(const std::string& message, int errorCode)13 : m_message(message), m_errorCode(errorCode) {}1415 // Override what() to provide error message16 const char* what() const noexcept override {17 return m_message.c_str();18 }1920 // Additional method to get error code21 int getErrorCode() const {22 return m_errorCode;23 }24};2526// More specific database exceptions27class ConnectionException : public DatabaseException {28public:29 ConnectionException(const std::string& message, int errorCode)30 : DatabaseException("Connection error: " + message, errorCode) {}31};3233class QueryException : public DatabaseException {34public:35 QueryException(const std::string& message, int errorCode)36 : DatabaseException("Query error: " + message, errorCode) {}37};3839// Simulate database operations40void connectToDatabase(const std::string& server) {41 if (server.empty()) {42 throw ConnectionException("Empty server name", 1001);43 }4445 if (server == "down") {46 throw ConnectionException("Server is down", 1002);47 }4849 std::cout << "Connected to " << server << std::endl;50}5152void executeQuery(const std::string& query) {53 if (query.empty()) {54 throw QueryException("Empty query", 2001);55 }5657 if (query == "DELETE") {58 throw QueryException("Dangerous query without WHERE clause", 2002);59 }6061 std::cout << "Executing query: " << query << std::endl;62}6364int main() {65 try {66 // Try to connect to database67 connectToDatabase("down");6869 // Try to execute a query70 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 }8485 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()
asnoexcept
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 Class | Header | Description |
---|---|---|
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 |
1#include <iostream>2#include <stdexcept>3#include <vector>4#include <string>5#include <new>67int main() {8 // std::out_of_range9 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 }1617 // std::invalid_argument18 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 }2425 // std::bad_alloc26 try {27 // Try to allocate a very large amount of memory28 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 }3435 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.
1#include <iostream>2#include <stdexcept>34// Function that doesn't throw exceptions5void safeFunction() noexcept {6 std::cout << "This function is guaranteed not to throw exceptions" << std::endl;7}89// Function that might throw exceptions10void riskyFunction() {11 if (rand() % 2 == 0) {12 throw std::runtime_error("Something went wrong");13 }14 std::cout << "Function completed successfully" << std::endl;15}1617// Function with conditional noexcept18template <typename T>19T add(T a, T b) noexcept(noexcept(a + b)) {20 return a + b;21}2223// Checking if a function is noexcept24template <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}3233int main() {34 // Using noexcept functions35 safeFunction();3637 // Using potentially throwing functions38 try {39 riskyFunction();40 }41 catch (const std::exception& e) {42 std::cout << "Caught exception: " << e.what() << std::endl;43 }4445 // Checking noexcept with different types46 checkNoexcept<int>();47 checkNoexcept<std::string>();4849 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.
1#include <iostream>2#include <stdexcept>3#include <string>45class Resource {6private:7 std::string m_name;89public: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 }1617 ~Resource() {18 std::cout << "Resource '" << m_name << "' destroyed" << std::endl;19 }20};2122class Application {23private:24 Resource m_resource1;25 Resource m_resource2;2627public:28 // Function try block for constructor29 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 exception36 throw;37 }3839 // Function try block for a regular method40 void run() try {41 std::cout << "Application running..." << std::endl;42 // Potentially throwing code43 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-throw51 }52};5354int main() {55 try {56 // This will succeed57 Application app1("Database", "Network");58 app1.run();5960 // 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 }6667 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.
1#include <iostream>2#include <stdexcept>3#include <memory>4#include <vector>56class Resource {7private:8 std::string m_name;910public:11 Resource(const std::string& name) : m_name(name) {12 std::cout << "Resource '" << m_name << "' acquired" << std::endl;13 }1415 ~Resource() {16 std::cout << "Resource '" << m_name << "' released" << std::endl;17 }1819 void use() {20 std::cout << "Using resource '" << m_name << "'" << std::endl;21 }22};2324// Poor exception safety - resources may leak25void unsafeFunction() {26 Resource* r1 = new Resource("Memory");27 Resource* r2 = new Resource("Database");2829 // If an exception is thrown here, r1 and r2 will leak30 throw std::runtime_error("Something went wrong");3132 // This code is never reached33 delete r2;34 delete r1;35}3637// Better exception safety with RAII and smart pointers38void safeFunction() {39 // Resources automatically managed40 std::unique_ptr<Resource> r1 = std::make_unique<Resource>("Memory");41 auto r2 = std::make_unique<Resource>("Database");4243 // Even if an exception is thrown, resources will be properly released44 throw std::runtime_error("Something went wrong");4546 // No need to manually release resources47}4849int main() {50 // Test unsafe function51 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 }5960 std::cout << "\n------------------------\n" << std::endl;6162 // Test safe function63 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 }7172 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)
1#include <iostream>2#include <stdexcept>3#include <fstream>4#include <string>5#include <memory>67// Good: Custom exception derived from std::exception8class 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) {}1314 const std::string& getFilename() const { return m_filename; }1516private:17 std::string m_filename;18};1920// Good: Using RAII for resource management21class FileReader {22private:23 std::ifstream m_file;24 std::string m_filename;2526public: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 }3334 // No need for explicit close - RAII handles it in destructor35 ~FileReader() {36 if (m_file.is_open()) {37 m_file.close();38 }39 }4041 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};5253// Good: Functions clearly document exception behavior54void processFile(const std::string& filename) {55 try {56 // Small, focused try block57 FileReader reader(filename);5859 // Process the file60 std::string line = reader.readLine();61 std::cout << "First line: " << line << std::endl;62 }63 // Good: Catching by const reference64 catch (const FileError& e) {65 std::cout << "File error: " << e.what() << std::endl;6667 // Log the error, attempt recovery, or rethrow68 }69 catch (const std::exception& e) {70 std::cout << "Unexpected error: " << e.what() << std::endl;71 throw; // Rethrow to let caller handle it72 }73}7475int 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 }8283 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 moreUnderstanding memory allocation and deallocation in C++.
Learn moreExplore object-oriented programming principles in C++.
Learn more