Try-Catch Basics
Exception handling is C++'s mechanism for dealing with runtime errors gracefully. Instead of crashing your program when something goes wrong, exceptions let you detect, report, and recover from errors in a structured way.
What is Exception Handling?
Imagine you're writing a program that reads a file. What happens if the file doesn't exist? Or if the user enters text when you expected a number? These are exceptional situations that your program needs to handle gracefully instead of crashing.
Exception
An exception is an object that represents an error or unexpected condition that occurs during program execution. When an error occurs, the code "throws" an exception, which propagates up the call stack until it's "caught" by an appropriate handler.
Key Components: try (code that might fail), throw
(signal an error), catch (handle the error)
Exception handling separates error-handling code from normal code, making your programs cleaner and easier to maintain. Instead of checking return codes after every function call, you can focus on the happy path and handle errors in a centralized location.
The Try-Catch Block
The fundamental structure of exception handling uses try and catch blocks.
Code that might throw an exception goes in the try block, and error handling code
goes in the catch block.
#include <iostream>
#include <stdexcept>
int main() {
try {
// Code that might throw an exception
int divisor = 0;
if (divisor == 0) {
throw std::runtime_error("Cannot divide by zero!");
}
int result = 10 / divisor;
std::cout << "Result: " << result << std::endl;
}
catch (const std::runtime_error& e) {
// Handle the exception
std::cout << "Error caught: " << e.what() << std::endl;
}
std::cout << "Program continues normally..." << std::endl;
return 0;
}
// Output: Error caught: Cannot divide by zero!
// Output: Program continues normally...
Multiple Catch Blocks
You can have multiple catch blocks to handle different exception types. The catch blocks are checked in order, and the first matching one is executed. Always order catch blocks from most specific to most general.
#include <iostream>
#include <stdexcept>
#include <string>
void processInput(int value) {
if (value < 0) {
throw std::invalid_argument("Value cannot be negative");
}
if (value > 100) {
throw std::out_of_range("Value must be between 0 and 100");
}
std::cout << "Processing value: " << value << std::endl;
}
int main() {
try {
processInput(150); // Will throw out_of_range
}
catch (const std::invalid_argument& e) {
std::cout << "Invalid argument: " << e.what() << std::endl;
}
catch (const std::out_of_range& e) {
std::cout << "Out of range: " << e.what() << std::endl;
}
catch (const std::exception& e) {
std::cout << "General error: " << e.what() << std::endl;
}
return 0;
}
// Output: Out of range: Value must be between 0 and 100
The Catch-All Handler
Use catch(...) to catch any exception type. This is useful as a last resort
to prevent unhandled exceptions from crashing your program, but you lose access to the
exception object.
#include <iostream>
void riskyOperation() {
throw 42; // Throwing an int (not recommended, but possible)
}
int main() {
try {
riskyOperation();
}
catch (const std::exception& e) {
std::cout << "Standard exception: " << e.what() << std::endl;
}
catch (...) {
// Catches anything not caught above
std::cout << "Unknown exception caught!" << std::endl;
}
return 0;
}
// Output: Unknown exception caught!
const std::exception&)
to avoid object slicing and unnecessary copying. Put catch(...) last as a safety net.
Exception Propagation
When an exception is thrown, it propagates up the call stack until a matching catch block is found. This is called stack unwinding. During unwinding, local objects are destroyed in reverse order of construction.
#include <iostream>
#include <stdexcept>
void level3() {
std::cout << "Entering level3" << std::endl;
throw std::runtime_error("Error in level3!");
std::cout << "Exiting level3" << std::endl; // Never executed
}
void level2() {
std::cout << "Entering level2" << std::endl;
level3(); // Exception propagates through here
std::cout << "Exiting level2" << std::endl; // Never executed
}
void level1() {
std::cout << "Entering level1" << std::endl;
try {
level2();
}
catch (const std::runtime_error& e) {
std::cout << "Caught in level1: " << e.what() << std::endl;
}
std::cout << "Exiting level1" << std::endl;
}
int main() {
level1();
return 0;
}
// Output: Entering level1
// Output: Entering level2
// Output: Entering level3
// Output: Caught in level1: Error in level3!
// Output: Exiting level1
Practice Questions: Try-Catch Basics
Problem: Create a function safeDivide that divides two integers and handles division by zero.
View Solution
#include <iostream>
#include <stdexcept>
double safeDivide(int numerator, int denominator) {
if (denominator == 0) {
throw std::invalid_argument("Division by zero!");
}
return static_cast<double>(numerator) / denominator;
}
int main() {
try {
std::cout << safeDivide(10, 2) << std::endl; // 5
std::cout << safeDivide(10, 0) << std::endl; // Throws
}
catch (const std::invalid_argument& e) {
std::cout << "Error: " << e.what() << std::endl;
}
return 0;
}
Problem: Write a function that parses a string to int and handles both empty strings and non-numeric strings.
View Solution
#include <iostream>
#include <string>
#include <stdexcept>
int parseInteger(const std::string& str) {
if (str.empty()) {
throw std::invalid_argument("Empty string");
}
try {
size_t pos;
int result = std::stoi(str, &pos);
if (pos != str.length()) {
throw std::invalid_argument("Non-numeric characters found");
}
return result;
}
catch (const std::out_of_range&) {
throw std::out_of_range("Number too large");
}
}
int main() {
std::string inputs[] = {"42", "", "12abc", "999999999999"};
for (const auto& input : inputs) {
try {
std::cout << "'" << input << "' = " << parseInteger(input) << std::endl;
}
catch (const std::exception& e) {
std::cout << "'" << input << "' error: " << e.what() << std::endl;
}
}
return 0;
}
Problem: Create a wrapper class for an array that throws exceptions for out-of-bounds access.
View Solution
#include <iostream>
#include <stdexcept>
#include <array>
template<typename T, size_t N>
class SafeArray {
private:
std::array<T, N> data;
public:
T& at(size_t index) {
if (index >= N) {
throw std::out_of_range("Index " + std::to_string(index) +
" out of bounds (size: " + std::to_string(N) + ")");
}
return data[index];
}
const T& at(size_t index) const {
if (index >= N) {
throw std::out_of_range("Index out of bounds");
}
return data[index];
}
size_t size() const { return N; }
};
int main() {
SafeArray<int, 5> arr;
for (size_t i = 0; i < arr.size(); ++i) {
arr.at(i) = i * 10;
}
try {
std::cout << arr.at(3) << std::endl; // 30
std::cout << arr.at(10) << std::endl; // Throws
}
catch (const std::out_of_range& e) {
std::cout << "Error: " << e.what() << std::endl;
}
return 0;
}
Throwing Exceptions
Learn how to signal errors in your code by throwing exceptions. The throw statement lets you create and propagate error conditions up the call stack to be handled by appropriate catch blocks.
The throw Statement
The throw keyword signals that an error has occurred. You can throw any type
of object, but it's best practice to throw objects derived from std::exception.
throw Statement
throw expression; creates an exception object and transfers control to the
nearest enclosing catch block that can handle it. The expression can be any copyable type.
Common forms: throw std::runtime_error("message"); or
throw MyException(data);
#include <iostream>
#include <stdexcept>
#include <string>
class BankAccount {
private:
std::string owner;
double balance;
public:
BankAccount(const std::string& name, double initial)
: owner(name), balance(initial) {}
void withdraw(double amount) {
if (amount <= 0) {
throw std::invalid_argument("Amount must be positive");
}
if (amount > balance) {
throw std::runtime_error("Insufficient funds! Balance: " +
std::to_string(balance));
}
balance -= amount;
std::cout << "Withdrew $" << amount << ". New balance: $" << balance << std::endl;
}
double getBalance() const { return balance; }
};
int main() {
BankAccount account("Alice", 100.0);
try {
account.withdraw(30); // OK
account.withdraw(200); // Throws runtime_error
}
catch (const std::exception& e) {
std::cout << "Transaction failed: " << e.what() << std::endl;
}
return 0;
}
// Output: Withdrew $30. New balance: $70
// Output: Transaction failed: Insufficient funds! Balance: 70.000000
What Can You Throw?
C++ allows throwing any copyable type, but different types have different implications. Here's a comparison of what you can throw and why standard exceptions are preferred:
| Type | Example | Recommendation |
|---|---|---|
| std::exception subclass | throw std::runtime_error("msg"); |
Recommended |
| Custom exception class | throw MyException(data); |
Recommended |
| String literal | throw "Error!"; |
Avoid |
| Integer | throw 404; |
Not recommended |
| Pointer | throw new Error(); |
Dangerous |
// Different ways to throw - only some are recommended!
// GOOD: Standard exceptions with descriptive messages
throw std::runtime_error("Connection timeout after 30 seconds");
throw std::invalid_argument("Age cannot be negative");
throw std::out_of_range("Index 10 is out of valid range [0, 5)");
// AVOID: Primitive types lack context
throw 404; // What does 404 mean? No message!
throw "Error!"; // C-string, not compatible with .what()
// DANGEROUS: Pointers cause memory leaks
throw new std::runtime_error("msg"); // Memory leak if not deleted!
Rethrowing Exceptions
Sometimes you want to partially handle an exception and then pass it along. Use throw;
(with no argument) to rethrow the current exception, preserving its type and information.
#include <iostream>
#include <stdexcept>
void processFile(const std::string& filename) {
// Simulate file processing
throw std::runtime_error("File not found: " + filename);
}
void loadConfiguration() {
try {
processFile("config.json");
}
catch (const std::exception& e) {
std::cout << "Logging error: " << e.what() << std::endl;
throw; // Rethrow the same exception
}
}
int main() {
try {
loadConfiguration();
}
catch (const std::exception& e) {
std::cout << "Application error: " << e.what() << std::endl;
}
return 0;
}
// Output: Logging error: File not found: config.json
// Output: Application error: File not found: config.json
throw; preserves the original exception
type. Using throw e; creates a copy, which can cause slicing if e is
a base class reference to a derived exception.
The noexcept Specifier
The noexcept specifier tells the compiler that a function won't throw exceptions.
This enables optimizations and is required for certain operations like move constructors in
STL containers.
#include <iostream>
#include <vector>
// This function promises not to throw
int add(int a, int b) noexcept {
return a + b;
}
// Conditional noexcept based on expression
template<typename T>
void swap(T& a, T& b) noexcept(noexcept(T(std::move(a)))) {
T temp = std::move(a);
a = std::move(b);
b = std::move(temp);
}
// Check at compile time if a function is noexcept
class MyClass {
public:
void safeMethod() noexcept { }
void unsafeMethod() { throw std::runtime_error("oops"); }
};
int main() {
std::cout << std::boolalpha;
std::cout << "add is noexcept: " << noexcept(add(1, 2)) << std::endl;
MyClass obj;
std::cout << "safeMethod is noexcept: " << noexcept(obj.safeMethod()) << std::endl;
std::cout << "unsafeMethod is noexcept: " << noexcept(obj.unsafeMethod()) << std::endl;
return 0;
}
// Output: add is noexcept: true
// Output: safeMethod is noexcept: true
// Output: unsafeMethod is noexcept: false
- Move constructors and move assignment
- Swap functions
- Destructors (implicit noexcept)
- Simple getters and setters
- Mathematical operations
- Functions that allocate memory
- Functions that do I/O operations
- Functions calling external libraries
- Virtual functions (may be overridden)
- When you're not sure
Practice Questions: Throwing Exceptions
Problem: Write a function that validates age (must be 0-150) and throws appropriate exceptions.
View Solution
#include <iostream>
#include <stdexcept>
void validateAge(int age) {
if (age < 0) {
throw std::invalid_argument("Age cannot be negative");
}
if (age > 150) {
throw std::out_of_range("Age cannot exceed 150");
}
std::cout << "Valid age: " << age << std::endl;
}
int main() {
int ages[] = {25, -5, 200, 100};
for (int age : ages) {
try {
validateAge(age);
}
catch (const std::exception& e) {
std::cout << "Invalid age " << age << ": " << e.what() << std::endl;
}
}
return 0;
}
Problem: Create a simple stack that throws exceptions on underflow and overflow.
View Solution
#include <iostream>
#include <stdexcept>
#include <array>
template<typename T, size_t MaxSize>
class Stack {
private:
std::array<T, MaxSize> data;
size_t topIndex = 0;
public:
void push(const T& value) {
if (topIndex >= MaxSize) {
throw std::overflow_error("Stack overflow: capacity " +
std::to_string(MaxSize) + " exceeded");
}
data[topIndex++] = value;
}
T pop() {
if (topIndex == 0) {
throw std::underflow_error("Stack underflow: stack is empty");
}
return data[--topIndex];
}
bool empty() const noexcept { return topIndex == 0; }
size_t size() const noexcept { return topIndex; }
};
int main() {
Stack<int, 3> stack;
try {
stack.push(1);
stack.push(2);
stack.push(3);
stack.push(4); // Throws overflow
}
catch (const std::overflow_error& e) {
std::cout << e.what() << std::endl;
}
return 0;
}
Problem: Create a logging wrapper that catches, logs, and rethrows exceptions without losing type information.
View Solution
#include <iostream>
#include <stdexcept>
#include <functional>
#include <chrono>
#include <ctime>
void logException(const std::string& context, const std::exception& e) {
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
std::cout << "[" << std::ctime(&time);
std::cout << "\b] " << context << ": " << e.what() << std::endl;
}
template<typename Func>
auto withLogging(const std::string& context, Func&& func)
-> decltype(func()) {
try {
return func();
}
catch (const std::exception& e) {
logException(context, e);
throw; // Preserves original exception type
}
}
int riskyOperation(int x) {
if (x < 0) throw std::invalid_argument("Negative value");
if (x > 100) throw std::out_of_range("Value too large");
return x * 2;
}
int main() {
try {
auto result = withLogging("riskyOperation", []() {
return riskyOperation(-5);
});
}
catch (const std::invalid_argument& e) {
std::cout << "Caught invalid_argument (type preserved!)" << std::endl;
}
return 0;
}
Standard Exception Hierarchy
C++ provides a rich hierarchy of standard exception classes in the <stdexcept>
header. Understanding these built-in exceptions helps you write more consistent and predictable
error handling code.
The std::exception Base Class
All standard exceptions inherit from std::exception, which provides a common
interface through the what() virtual method. This allows you to catch any
standard exception with a single handler.
std::exception
std::exception is the base class for all standard C++ exceptions. It provides
the virtual const char* what() const noexcept method that returns a description
of the error.
Key method: what() - Returns a C-string describing the exception
#include <iostream>
#include <exception>
#include <stdexcept>
#include <vector>
void demonstrateExceptions() {
std::vector<int> vec = {1, 2, 3};
try {
// This throws std::out_of_range
std::cout << vec.at(10) << std::endl;
}
catch (const std::exception& e) {
// Catches ANY standard exception
std::cout << "Exception type: " << typeid(e).name() << std::endl;
std::cout << "Message: " << e.what() << std::endl;
}
}
int main() {
demonstrateExceptions();
return 0;
}
// Output: Exception type: St12out_of_range (may vary by compiler)
// Output: Message: vector::_M_range_check: __n (which is 10) >= this->size() (which is 3)
Exception Hierarchy
The standard library divides exceptions into two main categories: logic_error
(bugs that could be prevented by checking conditions) and runtime_error
(errors that can only be detected during execution).
std::exception ├── std::logic_error (preventable errors) │ ├── std::invalid_argument - Invalid function argument │ ├── std::domain_error - Math domain error │ ├── std::length_error - Attempt to exceed max size │ ├── std::out_of_range - Index out of bounds │ └── std::future_error - Promise/future errors │ ├── std::runtime_error (runtime-only errors) │ ├── std::range_error - Range error in computation │ ├── std::overflow_error - Arithmetic overflow │ ├── std::underflow_error - Arithmetic underflow │ └── std::system_error - OS/system errors │ ├── std::bad_alloc - Memory allocation failure ├── std::bad_cast - Failed dynamic_cast ├── std::bad_typeid - typeid on null pointer └── std::bad_exception - Unexpected exception
Logic Errors
std::logic_error and its subclasses represent errors in program logic that
theoretically could be detected before the program runs. These are typically bugs that
should be fixed rather than caught.
#include <iostream>
#include <stdexcept>
#include <vector>
#include <cmath>
// std::invalid_argument - wrong type/value of argument
void setPercentage(int value) {
if (value < 0 || value > 100) {
throw std::invalid_argument("Percentage must be 0-100, got: " +
std::to_string(value));
}
std::cout << "Setting percentage to " << value << "%" << std::endl;
}
// std::out_of_range - index/position out of valid range
void accessElement(const std::vector<int>& vec, size_t index) {
if (index >= vec.size()) {
throw std::out_of_range("Index " + std::to_string(index) +
" is out of range [0, " + std::to_string(vec.size()) + ")");
}
std::cout << "Element: " << vec[index] << std::endl;
}
// std::length_error - attempt to exceed maximum size
void reserveMemory(std::vector<int>& vec, size_t count) {
if (count > vec.max_size()) {
throw std::length_error("Requested size exceeds maximum");
}
vec.reserve(count);
}
// std::domain_error - mathematical domain violation
double squareRoot(double x) {
if (x < 0) {
throw std::domain_error("Cannot compute square root of negative: " +
std::to_string(x));
}
return std::sqrt(x);
}
int main() {
try {
setPercentage(150);
} catch (const std::invalid_argument& e) {
std::cout << "Invalid argument: " << e.what() << std::endl;
}
try {
squareRoot(-5);
} catch (const std::domain_error& e) {
std::cout << "Domain error: " << e.what() << std::endl;
}
return 0;
}
Runtime Errors
std::runtime_error and its subclasses represent errors that can only be
detected during program execution, such as I/O failures, arithmetic issues, or external
resource problems.
#include <iostream>
#include <stdexcept>
#include <fstream>
#include <limits>
// std::runtime_error - general runtime failure
void openFile(const std::string& filename) {
std::ifstream file(filename);
if (!file.is_open()) {
throw std::runtime_error("Failed to open file: " + filename);
}
std::cout << "File opened successfully" << std::endl;
}
// std::overflow_error - arithmetic overflow
int safeMultiply(int a, int b) {
if (a > 0 && b > 0 && a > std::numeric_limits<int>::max() / b) {
throw std::overflow_error("Integer overflow: " + std::to_string(a) +
" * " + std::to_string(b));
}
return a * b;
}
// std::underflow_error - arithmetic underflow
unsigned int safeSubtract(unsigned int a, unsigned int b) {
if (b > a) {
throw std::underflow_error("Unsigned underflow: " + std::to_string(a) +
" - " + std::to_string(b));
}
return a - b;
}
int main() {
try {
openFile("nonexistent.txt");
} catch (const std::runtime_error& e) {
std::cout << "Runtime error: " << e.what() << std::endl;
}
try {
int result = safeMultiply(1000000, 1000000);
} catch (const std::overflow_error& e) {
std::cout << "Overflow: " << e.what() << std::endl;
}
try {
unsigned int result = safeSubtract(5, 10);
} catch (const std::underflow_error& e) {
std::cout << "Underflow: " << e.what() << std::endl;
}
return 0;
}
Memory and Type Exceptions
Some exceptions are thrown directly by C++ language features rather than library code. These include memory allocation failures and dynamic cast/typeid errors.
#include <iostream>
#include <new>
#include <typeinfo>
// std::bad_alloc - memory allocation failure
void demonstrateBadAlloc() {
try {
// Try to allocate impossibly large array
size_t huge = static_cast<size_t>(-1); // Maximum size_t
int* ptr = new int[huge];
}
catch (const std::bad_alloc& e) {
std::cout << "Allocation failed: " << e.what() << std::endl;
}
}
// std::bad_cast - failed dynamic_cast
class Base { public: virtual ~Base() {} };
class Derived : public Base { };
void demonstrateBadCast() {
try {
Base base;
// Reference dynamic_cast throws on failure
Derived& derived = dynamic_cast<Derived&>(base);
}
catch (const std::bad_cast& e) {
std::cout << "Bad cast: " << e.what() << std::endl;
}
}
// std::bad_typeid - typeid on null pointer
void demonstrateBadTypeid() {
try {
Base* ptr = nullptr;
// typeid on dereferenced null pointer throws
std::cout << typeid(*ptr).name() << std::endl;
}
catch (const std::bad_typeid& e) {
std::cout << "Bad typeid: " << e.what() << std::endl;
}
}
int main() {
demonstrateBadAlloc();
demonstrateBadCast();
demonstrateBadTypeid();
return 0;
}
invalid_argument for bad function inputs,
out_of_range for bad indices, runtime_error for file/network/external failures,
and logic_error for programming mistakes.
Practice Questions: Standard Exceptions
Problem: Write a function that validates an email address and throws appropriate exceptions.
View Solution
#include <iostream>
#include <stdexcept>
#include <string>
void validateEmail(const std::string& email) {
if (email.empty()) {
throw std::invalid_argument("Email cannot be empty");
}
size_t atPos = email.find('@');
if (atPos == std::string::npos) {
throw std::invalid_argument("Email must contain '@'");
}
if (atPos == 0) {
throw std::invalid_argument("Email cannot start with '@'");
}
size_t dotPos = email.find('.', atPos);
if (dotPos == std::string::npos || dotPos == email.length() - 1) {
throw std::invalid_argument("Email must have valid domain");
}
std::cout << "Valid email: " << email << std::endl;
}
int main() {
std::string emails[] = {"user@example.com", "", "invalid", "@test.com"};
for (const auto& email : emails) {
try {
validateEmail(email);
}
catch (const std::invalid_argument& e) {
std::cout << "Invalid '" << email << "': " << e.what() << std::endl;
}
}
return 0;
}
Problem: Create wrapper functions for vector operations that throw descriptive exceptions.
View Solution
#include <iostream>
#include <stdexcept>
#include <vector>
template<typename T>
class SafeVector {
private:
std::vector<T> data;
public:
void pushBack(const T& value) {
try {
data.push_back(value);
}
catch (const std::bad_alloc&) {
throw std::runtime_error("Memory allocation failed during push_back");
}
}
T& at(size_t index) {
if (index >= data.size()) {
throw std::out_of_range("Index " + std::to_string(index) +
" out of range. Size: " + std::to_string(data.size()));
}
return data[index];
}
T popBack() {
if (data.empty()) {
throw std::underflow_error("Cannot pop from empty vector");
}
T value = data.back();
data.pop_back();
return value;
}
size_t size() const noexcept { return data.size(); }
};
int main() {
SafeVector<int> vec;
vec.pushBack(10);
vec.pushBack(20);
try {
std::cout << vec.at(5) << std::endl;
}
catch (const std::out_of_range& e) {
std::cout << e.what() << std::endl;
}
return 0;
}
Problem: Write an exception handler that categorizes and logs different exception types appropriately.
View Solution
#include <iostream>
#include <stdexcept>
#include <typeinfo>
enum class ErrorCategory {
Logic, // Programming errors
Runtime, // External failures
Memory, // Allocation failures
Unknown // Non-standard exceptions
};
struct ExceptionInfo {
ErrorCategory category;
std::string typeName;
std::string message;
bool recoverable;
};
ExceptionInfo analyzeException(const std::exception& e) {
ExceptionInfo info;
info.typeName = typeid(e).name();
info.message = e.what();
if (dynamic_cast<const std::logic_error*>(&e)) {
info.category = ErrorCategory::Logic;
info.recoverable = false; // Usually a bug
}
else if (dynamic_cast<const std::runtime_error*>(&e)) {
info.category = ErrorCategory::Runtime;
info.recoverable = true; // Can often retry
}
else if (dynamic_cast<const std::bad_alloc*>(&e)) {
info.category = ErrorCategory::Memory;
info.recoverable = false; // Usually fatal
}
else {
info.category = ErrorCategory::Unknown;
info.recoverable = false;
}
return info;
}
void logException(const ExceptionInfo& info) {
const char* categories[] = {"LOGIC", "RUNTIME", "MEMORY", "UNKNOWN"};
std::cout << "[" << categories[static_cast<int>(info.category)] << "] "
<< info.typeName << ": " << info.message
<< (info.recoverable ? " (recoverable)" : " (fatal)") << std::endl;
}
int main() {
std::exception_ptr exceptions[] = {
std::make_exception_ptr(std::invalid_argument("bad arg")),
std::make_exception_ptr(std::runtime_error("file error")),
std::make_exception_ptr(std::bad_alloc())
};
for (auto& ep : exceptions) {
try {
std::rethrow_exception(ep);
}
catch (const std::exception& e) {
logException(analyzeException(e));
}
}
return 0;
}
Custom Exception Classes
When standard exceptions don't fit your needs, you can create custom exception classes. This gives you fine-grained control over error information and lets you build domain-specific exception hierarchies.
Why Create Custom Exceptions?
Custom exceptions let you carry additional context about errors, create meaningful hierarchies for your domain, and make your code more self-documenting. They're especially useful when you need to pass structured error information.
- You need to carry extra data (error codes, context)
- You want domain-specific exception types
- Standard exceptions don't describe your error well
- You need different handling for different error cases
- Always inherit from std::exception or subclass
- Override what() with noexcept
- Make exceptions copyable
- Store message internally (not as reference)
Creating a Basic Custom Exception
The simplest approach is to inherit from std::runtime_error or
std::logic_error, which handle the message storage for you.
#include <iostream>
#include <stdexcept>
#include <string>
// Simple custom exception inheriting from runtime_error
class FileException : public std::runtime_error {
public:
explicit FileException(const std::string& filename, const std::string& operation)
: std::runtime_error("File error: " + operation + " failed for '" + filename + "'"),
filename_(filename),
operation_(operation) {}
const std::string& getFilename() const noexcept { return filename_; }
const std::string& getOperation() const noexcept { return operation_; }
private:
std::string filename_;
std::string operation_;
};
void readFile(const std::string& filename) {
// Simulate file not found
throw FileException(filename, "open");
}
int main() {
try {
readFile("config.json");
}
catch (const FileException& e) {
std::cout << "Caught FileException:" << std::endl;
std::cout << " Message: " << e.what() << std::endl;
std::cout << " File: " << e.getFilename() << std::endl;
std::cout << " Operation: " << e.getOperation() << std::endl;
}
return 0;
}
// Output: Caught FileException:
// Output: Message: File error: open failed for 'config.json'
// Output: File: config.json
// Output: Operation: open
Exceptions with Error Codes
For more complex systems, you might want to include error codes that can be used for programmatic error handling or logging.
#include <iostream>
#include <stdexcept>
#include <string>
enum class DatabaseError {
ConnectionFailed = 1001,
QueryFailed = 1002,
RecordNotFound = 1003,
DuplicateKey = 1004,
Timeout = 1005
};
class DatabaseException : public std::runtime_error {
public:
DatabaseException(DatabaseError code, const std::string& message)
: std::runtime_error(formatMessage(code, message)),
errorCode_(code) {}
DatabaseError getErrorCode() const noexcept { return errorCode_; }
int getNumericCode() const noexcept {
return static_cast<int>(errorCode_);
}
bool isRecoverable() const noexcept {
return errorCode_ == DatabaseError::Timeout ||
errorCode_ == DatabaseError::ConnectionFailed;
}
private:
DatabaseError errorCode_;
static std::string formatMessage(DatabaseError code, const std::string& msg) {
return "[DB-" + std::to_string(static_cast<int>(code)) + "] " + msg;
}
};
void executeQuery(const std::string& query) {
// Simulate timeout
throw DatabaseException(DatabaseError::Timeout,
"Query timed out after 30 seconds");
}
int main() {
try {
executeQuery("SELECT * FROM users");
}
catch (const DatabaseException& e) {
std::cout << "Database error: " << e.what() << std::endl;
std::cout << "Error code: " << e.getNumericCode() << std::endl;
if (e.isRecoverable()) {
std::cout << "This error is recoverable - retrying..." << std::endl;
}
}
return 0;
}
// Output: Database error: [DB-1005] Query timed out after 30 seconds
// Output: Error code: 1005
// Output: This error is recoverable - retrying...
Building an Exception Hierarchy
For larger applications, create a hierarchy of exceptions that mirrors your domain model. This allows catching at different levels of specificity.
#include <iostream>
#include <stdexcept>
#include <string>
// Base exception for our application
class AppException : public std::runtime_error {
public:
explicit AppException(const std::string& msg) : std::runtime_error(msg) {}
};
// Network-related exceptions
class NetworkException : public AppException {
public:
explicit NetworkException(const std::string& msg) : AppException(msg) {}
};
class ConnectionException : public NetworkException {
public:
ConnectionException(const std::string& host, int port)
: NetworkException("Failed to connect to " + host + ":" + std::to_string(port)),
host_(host), port_(port) {}
const std::string& getHost() const noexcept { return host_; }
int getPort() const noexcept { return port_; }
private:
std::string host_;
int port_;
};
class TimeoutException : public NetworkException {
public:
TimeoutException(int seconds)
: NetworkException("Operation timed out after " + std::to_string(seconds) + "s"),
timeoutSeconds_(seconds) {}
int getTimeout() const noexcept { return timeoutSeconds_; }
private:
int timeoutSeconds_;
};
// Authentication exceptions
class AuthException : public AppException {
public:
explicit AuthException(const std::string& msg) : AppException(msg) {}
};
class InvalidCredentialsException : public AuthException {
public:
InvalidCredentialsException() : AuthException("Invalid username or password") {}
};
class SessionExpiredException : public AuthException {
public:
SessionExpiredException() : AuthException("Session has expired") {}
};
void handleException(const std::exception& e) {
// Handle at different levels of specificity
if (auto* ce = dynamic_cast<const ConnectionException*>(&e)) {
std::cout << "Connection failed to " << ce->getHost() << std::endl;
}
else if (auto* ne = dynamic_cast<const NetworkException*>(&e)) {
std::cout << "Network error: " << ne->what() << std::endl;
}
else if (auto* ae = dynamic_cast<const AuthException*>(&e)) {
std::cout << "Authentication error: " << ae->what() << std::endl;
}
else {
std::cout << "Unknown error: " << e.what() << std::endl;
}
}
int main() {
try {
throw ConnectionException("api.example.com", 443);
}
catch (const AppException& e) {
handleException(e);
}
try {
throw InvalidCredentialsException();
}
catch (const AppException& e) {
handleException(e);
}
return 0;
}
// Output: Connection failed to api.example.com
// Output: Authentication error: Invalid username or password
RAII and Exception Safety
The RAII (Resource Acquisition Is Initialization) pattern ensures resources are properly released even when exceptions occur. Smart pointers and scope guards are essential tools for exception-safe code.
#include <iostream>
#include <memory>
#include <stdexcept>
#include <fstream>
// RAII wrapper for file handle
class FileHandle {
public:
explicit FileHandle(const std::string& filename) : file_(filename) {
if (!file_.is_open()) {
throw std::runtime_error("Cannot open file: " + filename);
}
std::cout << "File opened" << std::endl;
}
~FileHandle() {
if (file_.is_open()) {
file_.close();
std::cout << "File closed (destructor)" << std::endl;
}
}
void write(const std::string& data) {
file_ << data;
if (!file_.good()) {
throw std::runtime_error("Write failed");
}
}
// Prevent copying
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
private:
std::ofstream file_;
};
void processData() {
FileHandle file("output.txt"); // RAII: opened here
file.write("Line 1\n");
file.write("Line 2\n");
// Simulate error
throw std::runtime_error("Processing failed!");
file.write("Line 3\n"); // Never reached
// File automatically closed by destructor, even with exception!
}
int main() {
try {
processData();
}
catch (const std::exception& e) {
std::cout << "Caught: " << e.what() << std::endl;
}
return 0;
}
// Output: File opened
// Output: File closed (destructor)
// Output: Caught: Processing failed!
1. No-throw guarantee: Function never throws (use noexcept)
2. Strong guarantee: If exception, state is unchanged (rollback)
3. Basic guarantee: No resource leaks, but state may change
4. No guarantee: Anything can happen (avoid this!)
Practice Questions: Custom Exceptions
Problem: Create a NegativeNumberException that stores the invalid value.
View Solution
#include <iostream>
#include <stdexcept>
#include <string>
class NegativeNumberException : public std::invalid_argument {
public:
explicit NegativeNumberException(double value)
: std::invalid_argument("Negative number not allowed: " + std::to_string(value)),
value_(value) {}
double getValue() const noexcept { return value_; }
private:
double value_;
};
double squareRoot(double x) {
if (x < 0) {
throw NegativeNumberException(x);
}
return std::sqrt(x);
}
int main() {
try {
std::cout << squareRoot(-25) << std::endl;
}
catch (const NegativeNumberException& e) {
std::cout << e.what() << std::endl;
std::cout << "Value was: " << e.getValue() << std::endl;
}
return 0;
}
Problem: Create exceptions for HTTP errors with status codes (404, 500, etc.).
View Solution
#include <iostream>
#include <stdexcept>
#include <string>
class HttpException : public std::runtime_error {
public:
HttpException(int statusCode, const std::string& message)
: std::runtime_error(std::to_string(statusCode) + " " + message),
statusCode_(statusCode) {}
int getStatusCode() const noexcept { return statusCode_; }
bool isClientError() const noexcept { return statusCode_ >= 400 && statusCode_ < 500; }
bool isServerError() const noexcept { return statusCode_ >= 500; }
private:
int statusCode_;
};
class NotFoundException : public HttpException {
public:
explicit NotFoundException(const std::string& resource)
: HttpException(404, "Not Found: " + resource) {}
};
class UnauthorizedException : public HttpException {
public:
UnauthorizedException() : HttpException(401, "Unauthorized") {}
};
class InternalServerException : public HttpException {
public:
explicit InternalServerException(const std::string& detail)
: HttpException(500, "Internal Server Error: " + detail) {}
};
int main() {
try {
throw NotFoundException("/api/users/999");
}
catch (const HttpException& e) {
std::cout << "HTTP " << e.getStatusCode() << ": " << e.what() << std::endl;
std::cout << "Client error: " << std::boolalpha << e.isClientError() << std::endl;
}
return 0;
}
Problem: Create a transaction class that rolls back on exception (strong guarantee).
View Solution
#include <iostream>
#include <stdexcept>
#include <vector>
#include <functional>
class Transaction {
public:
using Action = std::function<void()>;
using Rollback = std::function<void()>;
void addStep(Action action, Rollback rollback) {
action(); // Execute the action
rollbacks_.push_back(rollback); // Store rollback
}
void commit() {
rollbacks_.clear(); // Clear rollbacks on successful commit
std::cout << "Transaction committed" << std::endl;
}
~Transaction() {
// Rollback in reverse order if not committed
for (auto it = rollbacks_.rbegin(); it != rollbacks_.rend(); ++it) {
try {
(*it)();
}
catch (...) {
// Swallow exceptions in destructor
}
}
if (!rollbacks_.empty()) {
std::cout << "Transaction rolled back" << std::endl;
}
}
private:
std::vector<Rollback> rollbacks_;
};
int balance = 1000;
void transfer(int amount) {
Transaction tx;
int oldBalance = balance;
tx.addStep(
[amount]() {
balance -= amount;
std::cout << "Deducted " << amount << std::endl;
},
[oldBalance]() {
balance = oldBalance;
std::cout << "Restored balance to " << oldBalance << std::endl;
}
);
// Simulate failure
if (amount > 500) {
throw std::runtime_error("Transfer limit exceeded");
}
tx.commit();
}
int main() {
std::cout << "Initial balance: " << balance << std::endl;
try {
transfer(600); // Will fail and rollback
}
catch (const std::exception& e) {
std::cout << "Error: " << e.what() << std::endl;
}
std::cout << "Final balance: " << balance << std::endl;
return 0;
}
// Output: Initial balance: 1000
// Output: Deducted 600
// Output: Restored balance to 1000
// Output: Transaction rolled back
// Output: Error: Transfer limit exceeded
// Output: Final balance: 1000
Interactive Demo: Exception Flow
Visualize how exceptions propagate through your code. Select a scenario and watch the execution flow step-by-step.
Code Preview
// Select a scenario to see the code
Execution Flow
Click "Run Simulation" to see the flow
Key Takeaways
Try-Catch for Safety
Wrap risky code in try blocks and handle errors in catch blocks to prevent crashes
Throw Meaningful Errors
Use throw to signal errors with descriptive messages that help diagnose problems
Use Standard Exceptions
Leverage std::exception hierarchy for consistent, well-understood error types
Custom When Needed
Create custom exception classes for domain-specific errors with extra context
Catch by Reference
Always catch exceptions by const reference to avoid slicing and copying overhead
RAII for Cleanup
Use RAII pattern to ensure resources are properly released even when exceptions occur
Knowledge Check
Quick Quiz
Test what you've learned about C++ exception handling