Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Exception Handling in Constructors
Catch exceptions thrown from member initializer lists.
Function try blocks
Try and catch blocks handle most exception scenarios effectively, but there's one particular case where they fall short. Consider this example:
#include <iostream>
#include <string>
class Configuration
{
private:
int m_port;
public:
Configuration(int port) : m_port{port}
{
if (port <= 0)
{
throw std::string{"Port must be positive"}; // Exception thrown here
}
}
};
class Server : public Configuration
{
public:
Server(int port) : Configuration{port} // Configuration initialized in member initializer list
{
// What if Configuration construction fails and we want to handle it here?
}
};
int main()
{
try
{
Server server{-1};
}
catch (const std::string& error)
{
std::cout << "Error: " << error << '\n';
}
return 0;
}
In this example, derived class Server calls base class constructor Configuration, which can throw an exception. Because the creation of server has been placed inside a try block (in function main()), if Configuration throws an exception, main's try block catches it. Consequently, this program prints:
Error: Port must be positive
But what if we want to catch the exception inside Server? The call to base constructor Configuration happens via the member initializer list, before the Server constructor's body executes. There's no way to wrap a standard try block around it.
For this situation, we need a slightly modified try block called a function try block.
Function try blocks
Function try blocks allow you to establish an exception handler around the body of an entire function, rather than around a block of code.
The syntax is best shown by example:
#include <iostream>
#include <string>
class Configuration
{
private:
int m_port;
public:
Configuration(int port) : m_port{port}
{
if (port <= 0)
{
throw std::string{"Port must be positive"}; // Exception thrown here
}
}
};
class Server : public Configuration
{
public:
Server(int port) try : Configuration{port} // note addition of try keyword here
{
}
catch (...) // note this is at same level of indentation as the function itself
{
// Exceptions from member initializer list or
// from constructor body are caught here
std::cerr << "Server construction failed\n";
throw; // rethrow the existing exception
}
};
int main()
{
try
{
Server server{-1};
}
catch (const std::string& error)
{
std::cout << "Error: " << error << '\n';
}
return 0;
}
When this program runs, it produces:
Server construction failed Error: Port must be positive
Let's examine this program in detail.
First, note the addition of the try keyword before the member initializer list. This indicates that everything after that point (until the end of the function) should be considered inside the try block.
Second, note that the associated catch block is at the same level of indentation as the entire function. Any exception thrown between the try keyword and the end of the function body will be eligible to be caught here.
When the program runs, variable server begins construction, which calls Server's constructor (which uses a function try block). Server's constructor calls Configuration's constructor, which throws an exception. Because Configuration's constructor doesn't handle this exception, the exception propagates up the stack to Server's constructor, where it's caught by the function-level catch of Server's constructor. The catch block prints "Server construction failed", then rethrows the current exception up the stack, which is caught by the catch block in main(), which prints the error message.
Use function try blocks when you need a constructor to handle an exception thrown in the member initializer list.
With a regular catch block (inside a function), we have three options: throw a new exception, rethrow the current exception, or resolve the exception (by either a return statement or by letting control reach the end of the catch block).
A function-level catch block for a constructor must either throw a new exception or rethrow the existing exception—they are not allowed to resolve exceptions! Return statements are also not allowed, and reaching the end of the catch block will implicitly rethrow.
A function-level catch block for a destructor can throw, rethrow, or resolve the current exception via a return statement. Reaching the end of the catch block will implicitly rethrow.
A function-level catch block for other functions can throw, rethrow, or resolve the current exception via a return statement. Reaching the end of the catch block will implicitly resolve the exception for non-value (void) returning functions and produce undefined behavior for value-returning functions!
The following table summarizes the limitations and behavior of function-level catch blocks:
| Function type | Can resolve via return | Behavior at end of catch block |
|---|---|---|
| Constructor | No, must throw or rethrow | Implicit rethrow |
| Destructor | Yes | Implicit rethrow |
| Non-value returning function | Yes | Resolve exception |
| Value-returning function | Yes | Undefined behavior |
Because behavior at the end of the catch block varies dramatically depending on the type of function (and includes undefined behavior for value-returning functions), we recommend never letting control reach the end of the catch block, and always explicitly throwing, rethrowing, or returning.
Avoid letting control reach the end of a function-level catch block. Instead, explicitly throw, rethrow, or return.
In the program above, if we hadn't explicitly rethrown the exception in the function-level catch block of the constructor, control would have reached the end of the function-level catch, and because this was a constructor, an implicit rethrow would have happened. The result would have been identical.
Although function try blocks can be used with non-member functions as well, they typically aren't because there's rarely a need for this. They are almost exclusively used with constructors!
In the above example, if either Configuration or Server's constructor throws an exception, it will be caught by the try block around Server's constructor.
We can see this in the following example, where we're throwing an exception from class Server instead of class Configuration:
#include <iostream>
#include <string>
class Configuration
{
private:
int m_port;
public:
Configuration(int port) : m_port{port}
{
}
};
class Server : public Configuration
{
public:
Server(int port) try : Configuration{port} // note addition of try keyword here
{
if (port <= 0) // moved validation from Configuration to Server
{
throw std::string{"Port must be positive"};
}
}
catch (...)
{
std::cerr << "Server construction failed\n";
// If an exception isn't explicitly thrown here,
// the current exception will be implicitly rethrown
}
};
int main()
{
try
{
Server server{-1};
}
catch (const std::string& error)
{
std::cout << "Error: " << error << '\n';
}
return 0;
}
We get the same output:
Server construction failed Error: Port must be positive
Don't use function try to clean up resources
When construction of an object fails, the destructor of the class is not called. Consequently, you might be tempted to use a function try block as a way to clean up a class that partially allocated resources before failing. However, referring to members of the failed object is considered undefined behavior since the object is "dead" before the catch block executes. This means you can't use function try blocks to clean up after a class. If you want to clean up after a class, follow the standard rules for cleaning up classes that throw exceptions.
Function try blocks are useful primarily for logging failures before passing the exception up the stack, or for changing the type of exception thrown.
Summary
Function try blocks: A function try block allows you to establish an exception handler around the body of an entire function, including the member initializer list for constructors. Use the try keyword before the member initializer list, and place the catch block(s) at the same indentation level as the function itself.
Primary use case: Function try blocks are almost exclusively used with constructors to handle exceptions thrown during member initialization, which occurs before the constructor body executes.
Constructor limitations: Function-level catch blocks for constructors must either throw a new exception or rethrow the existing exception—they cannot resolve exceptions. Reaching the end of the catch block will implicitly rethrow.
Destructor behavior: Function-level catch blocks for destructors can throw, rethrow, or resolve via return. Reaching the end will implicitly rethrow.
Non-value returning function behavior: Function-level catch blocks for void functions can throw, rethrow, or resolve via return. Reaching the end will resolve the exception.
Value-returning function behavior: Function-level catch blocks for value-returning functions can throw, rethrow, or resolve via return. Reaching the end produces undefined behavior.
Best practice: Never let control reach the end of a function-level catch block. Always explicitly throw, rethrow, or return to avoid implicit behavior that varies by function type.
Not for cleanup: Don't use function try blocks to clean up resources from failed construction. Referring to members of the failed object is undefined behavior. Use RAII with member objects instead.
Function try blocks provide a specialized mechanism for handling exceptions from member initialization, filling a gap where regular try blocks cannot reach.
Exception Handling in Constructors - Quiz
Test your understanding of the lesson.
Practice Exercises
Function Try Blocks in Constructors
Use function try blocks to catch exceptions thrown during member initialization in a derived class constructor.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!