Exception Handling in Constructors
Catch exceptions thrown from member initializer lists.
What Is a Function Try Block?
A function try block is a try region whose scope is an entire function rather than a statement block inside it. For a constructor, the region begins before the member initializer list, so a handler written this way sees exceptions thrown while base classes and data members are being built, not just exceptions thrown by statements in the constructor body.
Everything else in this chapter has treated a try block as something you place inside a function. That works because the code you want to guard is made of statements, and statements live in blocks. Constructors break the assumption: a large part of what a constructor does happens before its opening brace is ever reached.
The Gap an Ordinary Try Block Cannot Reach
Construction of a derived object runs in a fixed order:
- the base class subobject is constructed
- the data members are constructed, in declaration order
- the constructor body runs
Steps 1 and 2 are driven by the member initializer list. By the time control arrives at the opening brace of the body, they are already finished. So if a base class constructor throws, nothing in the body has run yet:
#include <iostream>
#include <stdexcept>
class Oscillator
{
private:
double m_hertz{};
public:
explicit Oscillator(double hertz) : m_hertz{hertz}
{
if (hertz <= 0.0)
{
throw std::invalid_argument{"frequency must be positive"};
}
std::cout << "oscillator armed at " << m_hertz << " Hz\n";
}
};
class Voice : public Oscillator
{
public:
explicit Voice(double hertz) : Oscillator{hertz}
{
std::cout << "voice body entered\n";
}
};
int main()
{
try
{
Voice lead{-40.0};
}
catch (const std::invalid_argument& err)
{
std::cout << "main reported: " << err.what() << '\n';
}
return 0;
}
The exception travels straight past Voice and lands in main:
main reported: frequency must be positive
Notice what is missing: "voice body entered" never prints. That is the whole difficulty. Voice has no opportunity to react, because the only place it could put a try block is the body, and the body is dead code once the initializer list has thrown. The sketch below does not compile, and it shows why the obvious fix is not available either:
class Voice : public Oscillator
{
public:
explicit Voice(double hertz)
: try Oscillator{hertz} // there is no such syntax
{
}
};
The initializer list is not a block, so you cannot wrap one around it.
Moving the Try Keyword in Front of the Initializer List
The fix is to write try between the constructor's parameter list and the colon that starts the initializer list, and to put the handlers after the closing brace of the body, at the same indentation as the function itself:
#include <iostream>
#include <stdexcept>
class Oscillator
{
private:
double m_hertz{};
public:
explicit Oscillator(double hertz) : m_hertz{hertz}
{
if (hertz <= 0.0)
{
throw std::invalid_argument{"frequency must be positive"};
}
std::cout << "oscillator armed at " << m_hertz << " Hz\n";
}
};
class Voice : public Oscillator
{
public:
explicit Voice(double hertz)
try : Oscillator{hertz}
{
std::cout << "voice body entered\n";
}
catch (const std::exception& err)
{
std::cout << "voice gave up: " << err.what() << '\n';
throw;
}
};
int main()
{
try
{
Voice lead{-40.0};
}
catch (const std::invalid_argument& err)
{
std::cout << "main reported: " << err.what() << '\n';
}
return 0;
}
voice gave up: frequency must be positive
main reported: frequency must be positive
Two details carry all the meaning. The try keyword sits before the colon, which is what pulls the initializer list inside the guarded region. The catch is a peer of the function body rather than a statement within it, which is why it is indented level with explicit Voice, not one level deeper.
The bare throw; at the end of the handler sends the same exception onward, so main still sees the original std::invalid_argument. Without it the exception would still reach main, for reasons covered further down, but writing it makes the intent visible.
Reach for a function try block when a constructor needs to react to a failure in its member initializer list. That is the situation it exists for. On ordinary functions you can always place a normal
try block around the body instead, so there is little reason to use one there.
What the Handler Actually Covers
The guarded region runs from the try keyword to the closing brace of the body, so it spans all three construction steps. Base class failures, member failures, and failures inside the body all arrive at the same handler.
This program builds a Voice twice: the first attempt fails while constructing the Envelope member, the second gets all the way into the body before throwing.
#include <iostream>
#include <stdexcept>
class Oscillator
{
public:
explicit Oscillator(double hertz)
{
std::cout << " oscillator built at " << hertz << " Hz\n";
}
~Oscillator()
{
std::cout << " oscillator torn down\n";
}
};
class Envelope
{
public:
explicit Envelope(int attackMs)
{
if (attackMs < 0)
{
throw std::out_of_range{"attack time cannot be negative"};
}
std::cout << " envelope built\n";
}
};
class Voice : public Oscillator
{
private:
Envelope m_envelope;
public:
Voice(double hertz, int attackMs)
try : Oscillator{hertz}, m_envelope{attackMs}
{
if (hertz > 20000.0)
{
throw std::out_of_range{"frequency is beyond the audible range"};
}
std::cout << " voice body finished\n";
}
catch (const std::exception& err)
{
std::cout << " handler saw: " << err.what() << '\n';
throw;
}
};
int main()
{
std::cout << "attempt 1\n";
try
{
Voice pad{440.0, -5};
}
catch (const std::out_of_range&)
{
std::cout << " main gave up on attempt 1\n";
}
std::cout << "attempt 2\n";
try
{
Voice squeal{96000.0, 5};
}
catch (const std::out_of_range&)
{
std::cout << " main gave up on attempt 2\n";
}
return 0;
}
attempt 1
oscillator built at 440 Hz
oscillator torn down
handler saw: attack time cannot be negative
main gave up on attempt 1
attempt 2
oscillator built at 96000 Hz
envelope built
oscillator torn down
handler saw: frequency is beyond the audible range
main gave up on attempt 2
One handler, three possible origins. It is worth reading the ordering in attempt 1 closely, because the next section depends on it.
The Object Is Already Gone When the Handler Runs
Look at where "oscillator torn down" appears. In both attempts the base class destructor runs before the handler prints anything. That is the language rule: every subobject that was successfully constructed is destroyed as the exception leaves the constructor, and only then is the handler entered.
So by the time your handler body executes, there is no object left. The base class part is gone, any member that finished building is gone, and the destructor of the class itself will never run because the object never finished being constructed. Reading or writing a data member from inside a constructor's function try handler is undefined behavior.
Do not use a constructor's function try handler to release resources the half built object had acquired. The subobjects have already been destroyed, and touching a member from the handler is undefined behavior. Give every resource an owning member that cleans up in its own destructor instead, and the language will unwind correctly on its own.
That constraint is what limits the technique to two genuinely useful jobs: recording that construction failed, and replacing the exception with a different one before it continues outward.
Translating an Exception on the Way Out
A caller of Voice should not have to know that the frequency check lives in Oscillator. A function try block can catch the low level exception and throw a type that belongs to this class instead:
#include <iostream>
#include <stdexcept>
#include <string>
class Oscillator
{
public:
explicit Oscillator(double hertz)
{
if (hertz <= 0.0)
{
throw std::invalid_argument{"frequency must be positive"};
}
}
};
class VoiceSetupError : public std::runtime_error
{
public:
explicit VoiceSetupError(const std::string& detail)
: std::runtime_error{"voice setup failed: " + detail}
{
}
};
class Voice : public Oscillator
{
public:
explicit Voice(double hertz)
try : Oscillator{hertz}
{
}
catch (const std::invalid_argument& err)
{
throw VoiceSetupError{err.what()};
}
};
int main()
{
try
{
Voice bass{-12.5};
}
catch (const VoiceSetupError& err)
{
std::cout << err.what() << '\n';
}
return 0;
}
voice setup failed: frequency must be positive
Note that the handler copies the message out of err and never touches a member of the half built Voice, so it stays on the right side of the rule above.
What the Handler Is Allowed to Do
An ordinary handler inside a function has three ways to finish: throw something new, rethrow with throw;, or handle the exception and carry on. A function level handler keeps the first two everywhere, but the third is restricted, and the restriction is strictest exactly where the feature is most used.
In a constructor, the exception cannot be handled. There is no half constructed object to hand back to the caller, so the language refuses to let one exist. A return statement in a constructor's function level handler is rejected outright:
catch (const std::exception&)
{
std::cout << "handler reached the end without a throw\n";
return; // rejected
}
GCC reports error: cannot return from a handler of a function-try-block of a constructor.
Everywhere else, return is allowed and does resolve the exception. For a function that returns a value, returning explicitly is not merely allowed, it is the only correct way to finish:
#include <iostream>
#include <stdexcept>
int framesFor(int milliseconds)
try
{
if (milliseconds < 0)
{
throw std::out_of_range{"negative duration"};
}
return milliseconds * 48;
}
catch (const std::exception&)
{
return 0; // explicit, so control never falls off the end
}
int main()
{
std::cout << framesFor(25) << '\n';
std::cout << framesFor(-8) << '\n';
return 0;
}
1200
0
Falling Off the End Depends On the Kind of Function
If control reaches the handler's closing brace without a throw or a return, what happens next depends entirely on what kind of function the handler is attached to. Grouped by outcome:
| Reaching the closing brace of the handler | Applies to |
|---|---|
| Rethrows the current exception automatically | constructors and destructors |
| Discards the exception and returns normally | functions returning void |
| Undefined behavior | functions returning a value |
The constructor case is easy to confirm. Remove the throw; from the earlier Voice handler and the program behaves identically, because the rethrow happens anyway:
#include <iostream>
#include <stdexcept>
class Oscillator
{
public:
explicit Oscillator(double hertz)
{
if (hertz <= 0.0)
{
throw std::invalid_argument{"frequency must be positive"};
}
}
};
class Voice : public Oscillator
{
public:
explicit Voice(double hertz)
try : Oscillator{hertz}
{
}
catch (const std::exception& err)
{
std::cout << "handler logged: " << err.what() << '\n';
// no throw statement here
}
};
int main()
{
try
{
Voice lead{0.0};
}
catch (const std::invalid_argument& err)
{
std::cout << "main still received: " << err.what() << '\n';
}
return 0;
}
handler logged: frequency must be positive
main still received: frequency must be positive
A void function does the opposite. Here the handler ends without a throw, the exception is dropped, and the caller never learns anything went wrong:
#include <iostream>
#include <stdexcept>
void flushBuffer(int frames)
try
{
if (frames < 0)
{
throw std::out_of_range{"negative frame count"};
}
std::cout << "flushed " << frames << " frames\n";
}
catch (const std::exception& err)
{
std::cout << "flushBuffer swallowed: " << err.what() << '\n';
// no throw and no return
}
int main()
{
flushBuffer(-3);
std::cout << "main carried on\n";
return 0;
}
flushBuffer swallowed: negative frame count
main carried on
The value-returning case is the dangerous one, because falling off the end there is the same undefined behavior as falling off the end of any non-void function. GCC does warn (warning: control reaches end of non-void function [-Wreturn-type]), but a warning is a courtesy, not a guarantee.
Never let control reach the closing brace of a function level handler. Four function kinds, four different outcomes, one of them undefined, is far too much to keep in your head while reading unfamiliar code. End every handler with an explicit
throw, throw;, or return.
A destructor is implicitly
noexcept, so the automatic rethrow at the end of a destructor's function level handler calls std::terminate unless the destructor is declared noexcept(false). If you attach a function try block to a destructor, the handler must almost always end by swallowing the exception with a return.
Summary
The problem: a constructor's member initializer list runs before its body, so an ordinary try block written inside the body can never guard base class or member construction.
The syntax: put try after the parameter list and before the colon, and place the handlers after the body's closing brace, indented level with the function itself.
Coverage: one function level handler catches exceptions from base class construction, from member construction, and from the constructor body alike.
Ordering: subobjects that were successfully constructed are destroyed before the handler runs, so the object is already gone. Touching a member from the handler is undefined behavior, and function try blocks are therefore useless for cleanup.
Real uses: logging a construction failure, and translating an exception into a type that suits this class better.
Constructor handlers cannot resolve: return is a compile error there, and reaching the closing brace rethrows automatically.
Other function kinds: destructors rethrow at the closing brace too, void functions discard the exception, and value-returning functions have undefined behavior. Always finish a handler explicitly rather than relying on any of this.
Scope of the feature: function try blocks are legal on any function, but a constructor is the only place where they buy you something a plain try block cannot.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
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!