Exception handling in C++ allows programs to react to runtime errors in a structured and safe way.
try {
// protected code
}
catch (const ExceptionType& e) {
// handle specific exception
}
catch (...) {
// handle any exception
}The standard library provides a hierarchy of exception types derived from std::exception:
-
std::bad_alloc -
std::bad_cast -
std::bad_exception -
std::bad_typeid -
std::logic_errorstd::domain_errorstd::invalid_argumentstd::length_errorstd::out_of_range
-
std::runtime_errorstd::overflow_errorstd::underflow_errorstd::range_error
-
std::ios_base::failure
This exception is thrown by the allocation functions to when it failures to allocate memory.
try
{
while (true)
{
new int[100000000ul];
}
} catch (const std::bad_alloc& e)
{
std::cout << "Allocation failed: " << e.what() << '\n';
}nothrow is an empty class type and used as an argument for operator new and operator new[] to indicate that these functions shall not throw an exception on failure, but return a null pointer instead.
try
{
while (true)
{
new int[100000000ul]; // throwing overload
}
} catch (const std::bad_alloc& e)
{
std::cout << e.what() << '\n';
}
while (true)
{
int* p = new(std::nothrow) int[100000000ul]; // non-throwing overload
if (p == nullptr)
{
std::cout << "Allocation returned nullptr\n";
break;
}
}This exception is thrown when a dynamic_cast to a reference type fails, for instance when the types are not related by inheritance.
struct Foo { virtual ~Foo() {} };
struct Bar { virtual ~Bar() {} };
Bar b;
try
{
Foo& f = dynamic_cast<Foo&>(b);
} catch(const std::bad_cast& e)
{
std::cout << e.what() << '\n';
}This exception is thrown when a typeid operator is applied to a dereferenced null pointer value of a polymorphic type.
The type has to be polymorphic:
struct Foo
{
virtual void bar();
}; Foo* p = nullptr;
try
{
std::cout << typeid(*p).name() << '\n';
} catch(const std::bad_typeid& e) {
std::cout << e.what() << '\n';
}This defines a type of object to be thrown as exception when there are a consequence of faulty logic in your program.
int amount, available;
amount=10;
available=9;
try
{
if(amount>available)
throw std::logic_error( "logic error" );
}
catch ( std::exception &e )
{
std::cerr << "Caught: " << e.what( ) << std::endl;
std::cerr << "Type: " << typeid( e ).name( ) << std::endl;
};This defines a type of object to be thrown as exception in situations where the inputs are outside of the domain on which an operation is defined.
try
{
const double x = std::acos(2.0);
std::cout << x << '\n';
}
catch (std::domain_error& e)
{
std::cout << e.what() << '\n';
}This defines a type of object to be thrown as exception when an argument value has not been accepted.
binary wrongly represented by char X:
try
{
std::bitset<32> bitset(std::string("0101001X01010110000"));
}
catch (std::exception &err)
{
std::cerr<<"Caught "<<err.what()<<std::endl;
std::cerr<<"Type "<<typeid(err).name()<<std::endl;
}This defines a type of object to be thrown as exception as a result of attempts to exceed implementation defined length limits for some object. For instance vector throws a length_error if resized above max_size
try
{
std::vector<int> myvector;
myvector.resize(myvector.max_size()+1);
}
catch (const std::length_error& le)
{
std::cerr << "Length error: " << le.what() << '\n';
}This defines a type of object to be thrown as exception as consequence of attempt to access elements out of defined range.
std::vector<int> myvector(10);
try
{
myvector.at(20)=100; // vector::at throws an out-of-range
}
catch (const std::out_of_range& oor)
{
std::cerr << "Out of Range error: " << oor.what() << '\n';
}The only standard library components that throw std::overflow_error are std::bitset::to_ulong and std::bitset::to_ullong.
try
{
std::bitset<100> bitset;
bitset[99] = 1;
bitset[0] = 1;
// to_ulong(), converts a bitset object to the integer that would generate the sequence of bits
unsigned long Test = bitset.to_ulong();
}
catch(std::exception &err)
{
std::cerr<<"Caught "<<err.what()<<std::endl;
std::cerr<<"Type "<<typeid(err).name()<<std::endl;
}This defines a type of object to be thrown as exception where a result of a computation cannot be represented by the destination type.
The only standard library components that throw this exception are std::wstring_convert::from_bytes and std::wstring_convert::to_bytes.
try
{
throw std::range_error( "The range is in error!" );
}
catch (std::range_error &e)
{
std::cerr << "Caught: " << e.what( ) << std::endl;
std::cerr << "Type: " << typeid( e ).name( ) << std::endl;
} std::ifstream f("doesn't exist");
f.exceptions ( ifstream::badbit ); // No need to check failbit
try
{
f.exceptions(f.failbit);
}
catch (const std::ios_base::failure& e)
{
std::cout << "Caught an ios_base::failure.\n"
<< "Explanatory string: " << e.what() << '\n'
<< "Error code: " << e.code() << '\n';
}Ref: 1
... is a parameter pack and refers to zero or more template parameters. The ... will catch all exception.
try
{
some code
}
catch (Exception e)
{
some code
}
catch (...)//... Parameter Pack Expansion, will catch any exception
{
some code
}std::exception exposes one virtual function intended for overriding:
virtual const char* what() const noexcept;Important points:
- We override
what(), notthrow() throw()is an old C++98 exception specificationnoexceptis the modern replacement- The function must never throw
Correct modern definition:
struct CustomException : public std::exception {
const char* what() const noexcept override {
return "CustomException happened";
}
};Why what() is noexcept:
- It is usually called while another exception is already being handled
- Throwing during exception handling is fatal
- The standard enforces safety via
noexcept
Usage:
try {
throw CustomException();
} catch (const std::exception& e) {
std::cout << e.what() << '
';
}noexcept declares a non-throwing contract:
This function guarantees it will not throw exceptions.
If the guarantee is violated, the program calls std::terminate() immediately.
| Old syntax | Modern C++ |
|---|---|
throw() |
noexcept |
throw(T1, T2) |
❌ removed |
When an exception is thrown:
- Normal execution stops
- The runtime walks up the call stack
- Destructors of fully-constructed objects are invoked
- A matching
catchis searched
This cleanup process is called stack unwinding.
struct Bad {
~Bad() {
throw std::runtime_error("destructor failed");
}
};
void f() {
Bad b;
throw std::runtime_error("original error");
}Execution order:
original errortriggers stack unwinding~Bad()is invoked~Bad()throws another exception- Two active exceptions now exist
terminate called after throwing an instance of 'std::runtime_error'
what(): original error
Aborted (core dumped)
The C++ standard mandates this behavior: throwing while another exception is active is fatal.
Since C++11, destructors are implicitly:
~T() noexcept(true);Violating this guarantee leads to immediate program termination.
struct File {
FILE* f;
~File() {
if (std::fclose(f) != 0)
throw std::runtime_error("close failed");
}
};struct File {
FILE* f;
~File() noexcept {
if (f) std::fclose(f); // best-effort cleanup
}
void close() {
if (std::fclose(f) != 0)
throw std::runtime_error("close failed");
f = nullptr;
}
};Rule:
- Destructors clean up resources
- Errors are reported explicitly via functions
- Destructors must never throw
virtual const char* what() const noexcept;Reason:
- Called during exception handling
- Must be safe and non-throwing
MyType(MyType&&) noexcept;Why:
- Standard containers prefer move only if it is
noexcept - Otherwise they fall back to copying
- Enables faster reallocation and strong exception guarantees
| Context | Must be noexcept |
Reason |
|---|---|---|
| Destructors | Yes | Prevent fatal double exceptions |
what() |
Yes | Safe error reporting |
| Move ctor/assign | Strongly recommended | Container optimizations |
| Cleanup code | Yes | Stack unwinding safety |
A function must be noexcept if it can be executed during stack unwinding or cleanup, because throwing at that point immediately terminates the program.