skip to content

Construction is meant to yield either a valid object or no object at all. When it fails partway, what becomes of the memory, the already-initialized parts, and any reference that already escaped? Contrast how C++, Python, Java and Rust handle it.

level: principalimportance: should knowfreq 30%

answer

  1. four depths: unrepresentable / cleanup / reference-only / not at all
  2. C++: no own destructor, but members and bases are destroyed
  3. RAII member survives, raw handle in ctor body leaks
  4. Python __del__ runs on a half-built instance
  5. escaped this survives the throw; validate before you publish

basics

~20 s

The contract holds to different depths. C++ destroys completed bases and members but never runs the failed object's own destructor. Python already allocated the object in new, so a failing init still triggers del on a half-built instance. Java loses the reference but not anything the constructor registered. Rust makes the half-built value unrepresentable.

solid answer

~60 s

"Valid or nothing" is enforced at four different depths. - **C++**: throwing from a constructor means the object never existed, so **its own destructor does not run** -- but fully constructed bases and members are destroyed in reverse order, and already-built array elements too. That is precisely why a raw resource acquired in the constructor body leaks and an RAII member does not. - **Python**: `__new__` created the object before `__init__` ran, so a raising `__init__` leaves a real instance to be collected -- and `__del__` **is** called on it. A finalizer that assumes initialization completed then fails during collection, where the error is printed and swallowed. - **Java/C#**: no destructor problem, since the reference is never assigned and the object is garbage. But whatever the constructor registered -- a listener, a started thread, a static-map entry, `this` handed to a callback -- outlives the failure and now points at a half-built object. This is the shape of the historical finalizer attack. - **Rust**: construction returns `Result`, so failure produces no value; there is nothing partial to clean up.

code

cpp · 10 lines
cpp
struct Session {
    std::unique_ptr<Socket> primary;   // completed member: released on throw
    Socket* secondary = nullptr;       // raw: leaked on throw

    Session() : primary(std::make_unique<Socket>()) {
        secondary = new Socket();
        throw std::runtime_error("validation failed");
        // ~Session() never runs; ~unique_ptr does; secondary leaks
    }
};

go deeper

for a junior

Know that a constructor should validate its inputs and fail rather than produce a half-built object, and that failing means the caller gets nothing.

for a middle

Explain what cleanup happens on failure in the language you use, and why acquiring a raw resource in a constructor body is riskier than holding a self-cleaning member.

for a senior

Diagnose the escape routes -- a registered listener, a started thread, a finalizer running on half-built state -- and prescribe validate-before-acquire plus publish-after-construct.

for a principal

Rank the guarantees across languages, state which one your platform actually gives, and set codebase rules to the weakest guarantee: failure as a value where expressible, no side effects during construction, and cleanup owned by members rather than by the failure path.

## What the contract actually promises The useful reading of "a constructor either produces a valid object or fails" is: **after a failed construction, no one holds a reference to an invalid object, and no resource acquired along the way is left dangling.** Both halves can fail independently, and languages enforce them to very different depths. ## C++: cleanup is precise, and asymmetric on purpose If a constructor exits by throwing, the object is deemed never to have existed, so **its destructor is not called**. What *is* called are the destructors of every base class and every non-static member that had already completed, in reverse order of construction. For an array under construction, the elements already built are destroyed. This asymmetry is the entire argument for RAII: a `unique_ptr` or `fstream` member is a completed subobject and will be released, while a raw `new` or a raw file handle stored in the constructor body has no such guarantee and leaks. Two details matter in practice: a delegating constructor that has already completed makes the object "constructed" for cleanup purposes, so a throw from the delegating body *does* run the destructor; and a function-try-block on a constructor may not repair the object -- it must rethrow. ## Python: the object exists before initialization does Python separates allocation from initialization. `__new__` returns a real, fully allocated instance, and only then does `__init__` run on it. So when `__init__` raises, an object exists, is unreachable, and is collected like any other -- which means **`__del__` runs on a half-initialized instance**. A finalizer written as `def __del__(self): self.conn.close()` will raise `AttributeError` when the failure happened before `self.conn` was assigned, and exceptions during finalization are printed to stderr and otherwise ignored, so this produces noise rather than a diagnosable error. The defensive idiom is to make finalizers tolerate missing attributes, or better, to use context managers and explicit `close` so cleanup is not tied to finalization at all. ## Java and C#: the reference is safe, the side effects are not On the JVM and CLR, a throwing constructor means the `new` expression never yields a value, the variable is never assigned, and the partially built object becomes garbage -- no destructor complexity because there is no deterministic destruction. The contract holds perfectly *for the caller's reference*. It holds not at all for anything the constructor already did to the outside world: a listener registered with an event bus, a thread started, an entry written into a static cache, or `this` passed to a callback all survive, and every one of them is now a live route to an object whose validation failed. Historically this was even exploitable: because a subclass's finalizer ran even when the superclass constructor threw, an attacker could capture the half-built instance -- the finalizer attack -- which is why the classic mitigations are to make the class final, to validate before calling the superclass constructor, or to use a guard field checked by every method. Finalization's deprecation and removal has closed that particular door, but the general rule survives: **do not let a constructor publish the object or take an externally visible action**; do that in the creation function after construction succeeds. ## Rust and Go: the invalid state is not representable A Rust constructor is a function returning `Result<Self, E>`. On the failure path it returns `Err`, so no value of the type is ever produced; locals created along the way are dropped by the normal rules, and there is nothing half-built for anyone to observe. Failure is in the signature, so a caller cannot ignore it. Go's `func NewT(...) (*T, error)` has the same shape without the compiler's insistence -- a caller may discard the error, and the zero value of the type remains constructible regardless. ## The design conclusion Rank the guarantees you actually get: **unrepresentable** (Rust), **enforced with deterministic cleanup of completed parts** (C++), **enforced for the caller's reference only** (Java, C#), **not enforced against finalizers** (Python's `__del__`, and historically Java's). Then write to the weakest guarantee your language gives you: validate arguments before acquiring anything, acquire resources through members or wrappers that clean themselves up, never register the object with anything outside itself during construction, and prefer a creation function that returns a failure value where the language lets you express one. The failure path is the part of construction nobody tests, and it is the one where the object escapes.

  • Why does C++ deliberately skip the failed object's own destructor while still destroying its members?
    Because a destructor's job is to undo what its constructor completed, and the constructor did not complete -- running it would ask code to tear down state that may never have been established. Members and bases are different: each of those finished its own construction, so its destructor is exactly the right operation. The asymmetry is the reason RAII is not merely a style preference in C++ but the only reliable way to make constructor failure leak-free.
  • A constructor validates its arguments and throws, but it had already registered the object with an event bus. What is the state of the system?
    The caller has no reference and believes construction failed, while the event bus holds a live reference to an object that failed validation and may be missing fields entirely. The next event delivered invokes methods on it, producing a failure far from the cause. The rule that follows is that a constructor must not publish the object or perform externally visible side effects: do the registration in a creation function after the object is fully built and validated.
  • How would you design construction so the failure path needs no cleanup discipline at all?
    Compute and validate everything before any state is acquired, so the failure happens while nothing has been allocated. Where a resource must be acquired, hold it in a member or wrapper whose own cleanup is automatic, rather than in a raw handle. Best of all, express failure as a value -- a Result or an Optional returned by a creation function -- so the invalid object is never constructed and there is nothing to unwind.

saying these in an interview costs you the question

  • Believing C++ runs the destructor of an object whose constructor threw.
  • Assuming Python skips __del__ when __init__ raises; the object was already created by __new__.
  • Thinking a throwing constructor on the JVM guarantees nothing escaped, ignoring listeners, threads and static registries.
  • Treating construction failure as automatically leak-free because a garbage collector exists -- non-memory resources are not collected.
  • Claiming Go's constructor convention enforces the valid-or-fail contract; the error return can be discarded and the zero value is still constructible.

context