Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Try-Catch Exception Handling
Learn basic exception handling techniques for robust programs.
In the previous lesson, you learned why return codes create problems by mixing error handling with normal control flow. C++ exceptions solve this by separating error signaling from error handling using three keywords: throw, try, and catch.
Throwing exceptions
Think of exceptions like alert signals. When a smoke detector senses smoke, it emits a loud alarm. Similarly, when code encounters an error, it throws an exception to alert the program that something went wrong.
In C++, a throw statement signals that an exception has occurred. The term "raising an exception" is also commonly used.
You can throw any value:
throw 404; // Throw an integer
throw "Connection failed"; // Throw a C-style string
throw std::string{"Invalid configuration"}; // Throw a std::string
throw 3.14; // Throw a double
throw NetworkError{"Timeout"}; // Throw a custom class
Each throw statement signals a problem that needs handling. The thrown value typically describes what went wrong.
Detecting exceptions with try blocks
When a smoke alarm goes off, people notice and react. In C++, a try block acts as an observer, watching for exceptions thrown by code inside it.
Here's a try block:
try
{
// Code that might throw exceptions
throw "Network error";
}
The try block doesn't specify how to handle exceptions - it just watches for them. If any code inside throws an exception, the try block catches it and looks for a matching handler.
Handling exceptions with catch blocks
When a smoke alarm sounds, you take action - investigate, evacuate, or call emergency services. A catch block defines what action to take when an exception occurs.
Here's a catch block that handles string exceptions:
catch (const char* message)
{
std::cerr << "Error: " << message << '\n';
}
Try and catch blocks work together. The try block detects exceptions; the catch block handles them. A try block must have at least one catch block immediately following it, but can have multiple catches for different exception types.
Once a catch block handles an exception, execution continues normally with the first statement after all catch blocks.
Complete example with try, throw, and catch
Here's a full program demonstrating exception handling:
#include <iostream>
int main()
{
try
{
std::cout << "Connecting to server...\n";
throw "Connection timeout";
std::cout << "Connected successfully\n"; // Never executes
}
catch (const char* error)
{
std::cerr << "Network error: " << error << '\n';
}
catch (int errorCode)
{
std::cerr << "Error code: " << errorCode << '\n';
}
std::cout << "Program continues\n";
return 0;
}
Output:
Connecting to server...
Network error: Connection timeout
Program continues
The throw statement causes immediate transfer to the matching catch block. The "Connected successfully" message never prints because execution jumps directly to exception handling.
Catch block parameters
Catch blocks work like function parameters. The parameter receives the thrown value:
catch (int errorCode)
{
std::cout << "Received error code: " << errorCode << '\n';
}
For fundamental types, catch by value. For class types, catch by const reference to avoid copying:
catch (const std::string& message) // Catch by const reference
{
std::cerr << "Error: " << message << '\n';
}
If you don't need the exception value, omit the parameter name:
catch (int) // No parameter name
{
std::cerr << "An integer exception occurred\n";
}
This prevents unused variable warnings.
Multiple catch blocks
A single try block can have multiple catch blocks for different exception types:
#include <iostream>
#include <string>
int main()
{
try
{
throw 500; // Throw an integer
}
catch (const std::string& error)
{
std::cerr << "String exception: " << error << '\n';
}
catch (int code)
{
std::cerr << "Integer exception: " << code << '\n';
}
catch (double value)
{
std::cerr << "Double exception: " << value << '\n';
}
std::cout << "Continuing\n";
return 0;
}
Output:
Integer exception: 500
Continuing
The thrown exception is routed to the first matching catch block. Once handled, execution continues after all catch blocks.
No type conversion for exceptions
Unlike function arguments, exceptions don't undergo implicit type conversion:
try
{
throw 3.14; // Throws a double
}
catch (int value) // Won't match - int doesn't convert from double
{
std::cout << "Caught int: " << value << '\n';
}
This won't compile because the double exception doesn't match the int catch block. However, inheritance-based conversions (derived-to-base) do work.
A practical example: Configuration validation
Here's a realistic use of exceptions:
#include <iostream>
#include <string>
void validatePort(int port)
{
if (port < 1 || port > 65535)
throw "Port number out of valid range (1-65535)";
}
int main()
{
std::cout << "Enter port number: ";
int port{};
std::cin >> port;
try
{
validatePort(port);
std::cout << "Valid port: " << port << '\n';
std::cout << "Connecting...\n";
}
catch (const char* error)
{
std::cerr << "Configuration error: " << error << '\n';
}
return 0;
}
Valid input:
Enter port number: 8080
Valid port: 8080
Connecting...
Invalid input:
Enter port number: 99999
Configuration error: Port number out of valid range (1-65535)
The exception separates validation logic from normal flow. If validation fails, an exception bypasses the connection code and jumps to error handling.
Exceptions are handled immediately
When an exception is thrown, execution immediately transfers to the nearest try block's catch handlers:
#include <iostream>
int main()
{
try
{
std::cout << "Starting\n";
throw 42;
std::cout << "After throw\n"; // Never executes
}
catch (int value)
{
std::cout << "Caught: " << value << '\n';
}
std::cout << "Done\n";
return 0;
}
Output:
Starting
Caught: 42
Done
The code after throw never runs. Execution jumps immediately to the catch block.
Common catch block patterns
Catch blocks typically perform one of these actions:
1. Log and continue
catch (const char* error)
{
std::cerr << "Warning: " << error << '\n';
// Program continues
}
2. Return error code to caller
bool connectToServer()
{
try
{
// Connection code that might throw
return true;
}
catch (...)
{
return false;
}
}
3. Rethrow a different exception
catch (const char* lowLevelError)
{
std::cerr << "Low-level error: " << lowLevelError << '\n';
throw std::string{"High-level connection failed"};
}
4. Terminate gracefully
int main()
{
try
{
// Application code
}
catch (...)
{
std::cerr << "Fatal error, shutting down\n";
return 1;
}
return 0;
}
A more complex example: Resource initialization
Here's how exceptions simplify error handling when multiple operations can fail:
#include <iostream>
#include <string>
class ServerConfig
{
private:
std::string m_host;
int m_port;
int m_timeout;
public:
ServerConfig(const std::string& host, int port, int timeout)
: m_host{host}, m_port{port}, m_timeout{timeout}
{
if (m_host.empty())
throw "Host cannot be empty";
if (m_port < 1 || m_port > 65535)
throw "Invalid port number";
if (m_timeout < 0)
throw "Timeout cannot be negative";
}
void display() const
{
std::cout << "Host: " << m_host << '\n';
std::cout << "Port: " << m_port << '\n';
std::cout << "Timeout: " << m_timeout << "s\n";
}
};
int main()
{
try
{
ServerConfig config{"localhost", 8080, 30};
config.display();
std::cout << "\nTrying invalid config...\n";
ServerConfig badConfig{"", 8080, 30}; // Throws exception
badConfig.display(); // Never executes
}
catch (const char* error)
{
std::cerr << "Configuration error: " << error << '\n';
}
std::cout << "Program continues\n";
return 0;
}
Output:
Host: localhost
Port: 8080
Timeout: 30s
Trying invalid config...
Configuration error: Host cannot be empty
Program continues
The constructor validates parameters and throws if they're invalid. This prevents creating misconfigured objects.
Understanding exception flow
Exception handling follows this process:
- Code throws an exception using throw
- Execution immediately stops and searches for the nearest enclosing try block
- If found, the try block checks its catch handlers in order
- The first matching catch handler executes
- After the catch block completes, execution continues normally after all catch blocks
- If no matching catch is found, the search continues up the call stack
- If no handler is found anywhere, the program terminates
Here's a simple summary: throw signals a problem, try watches for problems, and catch handles them. After handling, normal execution resumes.
Recommendations
When using basic exception handling:
- Catch fundamental types by value
- Catch class types by const reference
- Use specific exception types when possible
- Keep catch blocks focused and simple
- Don't throw exceptions for normal control flow
- Document which exceptions your functions might throw
Summary
Exception handling in C++ uses three keywords:
- throw: Signals that an error has occurred
- try: Monitors code for exceptions
- catch: Handles specific exception types
This mechanism separates error signaling from error handling, making code cleaner and more maintainable than return-code-based approaches. In upcoming lessons, you'll learn about exception propagation, custom exception classes, and best practices for robust exception handling.
Try-Catch Exception Handling - Quiz
Test your understanding of the lesson.
Practice Exercises
Basic Exception Handling
Learn to throw and catch exceptions. Practice using try-catch blocks to handle error conditions gracefully.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!