Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Handling Unexpected Exceptions
Learn to handle uncaught exceptions and use catch-all handlers for robust programs.
Uncaught exceptions and catch-all handlers
By this point, you understand the fundamentals of exception handling. In this lesson, we'll explore edge cases that arise when exceptions aren't caught, and learn about catch-all handlers that can prevent program termination.
Uncaught exceptions
When a function throws an exception without handling it locally, the function assumes some caller up the stack will catch it. But what if that assumption is wrong? Consider this validation function without proper exception handling:
#include <iostream>
#include <string>
// Validates that port number is in allowed range
int validatePort(int port)
{
if (port < 1024 || port > 65535)
{
throw std::string{"Port must be between 1024 and 65535"};
}
return port;
}
int main()
{
std::cout << "Enter port number: ";
int port{};
std::cin >> port;
// No try/catch block to handle exceptions
std::cout << "Using port: " << validatePort(port) << '\n';
return 0;
}
If the user enters an invalid port like 80, validatePort() throws a string exception. Function validatePort() doesn't handle this exception itself, so the runtime searches up the call stack. However, main() also lacks an exception handler for this case.
When no exception handler can be found anywhere in the call stack, the program invokes std::terminate(), which immediately terminates the application. When this happens, the call stack may or may not be unwound! If unwinding doesn't occur, local variables won't be destroyed, and cleanup code in their destructors won't execute.
The call stack may or may not be unwound when an exception goes unhandled.
Without stack unwinding, objects with important destructors won't clean up properly, potentially causing resource leaks.
While it might seem counterintuitive to skip unwinding, there's a practical reason. Unhandled exceptions usually indicate serious programming errors that you want to debug. Preserving the stack state intact makes it much easier to diagnose the problem. If the stack were unwound, you'd lose crucial debugging information about the state that led to the unhandled exception.
When an unhandled exception occurs, the operating system notifies you in various ways depending on the platform. You might see an error message printed to the console, an error dialog box displayed, or the program might simply crash. Some operating systems handle this more gracefully than others. Regardless, this is a situation you want to avoid in production code.
This creates a dilemma:
- Functions can throw exceptions of infinitely many types, including custom classes you define.
- If an exception isn't caught, your program terminates immediately and may not clean up properly.
- Explicitly handling every possible exception type is tedious and impractical, especially for rare edge cases.
Fortunately, C++ provides catch-all handlers to address this problem. A catch-all handler functions like a regular catch block but uses ellipses (...) instead of a specific type. For this reason, it's also called an "ellipsis catch handler".
If you recall from the lesson on ellipses, they were used to pass arguments of any type to a function. In exception handling, they represent exceptions of any type whatsoever. Here's a simple demonstration:
#include <iostream>
int main()
{
try
{
throw 42; // throwing an int exception
}
catch (const std::string& message)
{
std::cout << "Caught string exception: " << message << '\n';
}
catch (...) // catch-all handler
{
std::cout << "Caught an exception of unknown type\n";
}
return 0;
}
Since no specific handler exists for int exceptions, the catch-all handler catches it instead. This program outputs:
Caught an exception of unknown type
The catch-all handler must always be placed last in the sequence of catch blocks. This ensures exceptions can be caught by type-specific handlers first if those handlers exist.
Often, catch-all handlers are left empty:
catch (...) {} // silently ignore any unexpected exceptions
This catches all unexpected exceptions, ensuring stack unwinding occurs and preventing abrupt termination. However, it doesn't perform any specific error handling or logging.
Using catch-all handlers to protect main()
One valuable use for catch-all handlers is wrapping the contents of main():
#include <iostream>
#include <string>
struct ServerState
{
// Server state data here
};
void processRequests(ServerState& state)
{
throw std::string{"Database connection lost"};
}
void saveServerState(ServerState& state)
{
// Persist server state to disk
std::cout << "Server state saved\n";
}
int main()
{
ServerState state{};
try
{
processRequests(state);
}
catch (...)
{
std::cerr << "Critical error: server shutting down\n";
}
saveServerState(state); // save state even if catch-all was triggered
return 0;
}
In this scenario, if processRequests() or any function it calls throws an unhandled exception, the catch-all handler catches it. The stack unwinds in an orderly fashion, ensuring local variables are destroyed properly. This also prevents immediate termination, giving us a chance to print an error message and save critical state before exiting.
If your program uses exceptions, consider adding a catch-all handler in main to ensure orderly behavior when unexpected exceptions occur.
When the catch-all handler catches something, assume your program is in an unpredictable state. Perform only essential cleanup, then terminate.
Unhandled exceptions signal unexpected situations that need investigation. Many debuggers can break on unhandled exceptions, allowing you to inspect the stack at the exact throw point. However, if you have a catch-all handler, all exceptions are technically handled, and you lose this valuable debugging information because the stack gets unwound.
Therefore, during debugging, it's useful to disable the catch-all handler. We can accomplish this using conditional compilation directives.
Here's one approach:
#include <iostream>
#include <string>
struct ServerState
{
// Server state data here
};
void processRequests(ServerState& state)
{
throw std::string{"Database connection lost"};
}
void saveServerState(ServerState& state)
{
std::cout << "Server state saved\n";
}
class ImpossibleException // cannot be instantiated
{
ImpossibleException() = delete;
};
int main()
{
ServerState state{};
try
{
processRequests(state);
}
#ifndef NDEBUG // in debug mode
catch (ImpossibleException) // compile in a catch that will never trigger
{
}
#else // in release mode
catch (...) // compile in the actual catch-all handler
{
std::cerr << "Critical error: server shutting down\n";
}
#endif
saveServerState(state);
return 0;
}
Syntactically, try blocks require at least one associated catch block. When the catch-all handler is conditionally compiled out, we either need to remove the try block entirely or provide an alternative catch block. The latter approach is cleaner.
For this purpose, we create class ImpossibleException which cannot be instantiated because it has a deleted default constructor and no other constructors. When NDEBUG is not defined (debug mode), we compile in a catch handler for ImpossibleException. Since we can't create an instance of ImpossibleException, this handler will never catch anything. Therefore, any exceptions that reach this point go unhandled, allowing the debugger to break at the throw site with the stack intact.
In release mode (when NDEBUG is defined), the actual catch-all handler is compiled in, providing the safety net for production use.
Summary
Unhandled exceptions: When no exception handler can be found anywhere in the call stack, the program invokes std::terminate(), which immediately terminates the application. The call stack may or may not be unwound, potentially preventing proper cleanup.
Catch-all handlers: A catch-all handler uses ellipses (...) instead of a specific exception type, catching exceptions of any type whatsoever. It must always be placed last in the sequence of catch blocks.
Protecting main(): One valuable use for catch-all handlers is wrapping the contents of main() to ensure orderly behavior when unexpected exceptions occur. This allows essential cleanup before termination.
Silent catching: Catch-all handlers can be left empty to silently ignore unexpected exceptions, ensuring stack unwinding occurs and preventing abrupt termination.
Debugging considerations: During debugging, catch-all handlers should be disabled using conditional compilation to allow debuggers to break on unhandled exceptions at the exact throw point with the stack intact.
Best practices: If your program uses exceptions, consider adding a catch-all handler in main to ensure orderly behavior. When the catch-all handler catches something, perform only essential cleanup and then terminate, as the program state may be unpredictable.
Catch-all handlers provide a safety net for unexpected exceptions, ensuring your program can perform critical cleanup before termination rather than crashing abruptly.
Handling Unexpected Exceptions - Quiz
Test your understanding of the lesson.
Practice Exercises
Catch-All Handler
Create a program that demonstrates catch-all handlers by throwing different types of exceptions and catching them with a catch-all handler.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!