Deinit and throwing initializers

Is there a deterministic way to know whether deinit will be called for an instance of a class with a throwing initializer when the init does throw?

My understanding was that, so long as a type was fully initialized, deinit would always be called. However, consider the following code:

struct Err: Error {
    let message: String
}

class MyClass {
    let x: Int

    init() throws {
        if 1 == 2 {
            throw Err(message: "beforeError")
        }
        self.x = 1

        throw Err(message: "afterErr")
    }

    deinit {
        print("in deinit")
    }
}

do {
    let c = try MyClass()
} catch {
    print("error: \(error)")
}

In this case (at least in Swift 5.2.4 on macOS), even though the type is fully initialized before the error is thrown, deinit is not called, and the output is:

error: Err(message: "afterErr")

So it seems my prior understanding was incorrect.

If you comment out the first error in the 1 == 2 block, the output becomes:

in deinit
error: Err(message: "afterErr")

This makes it seem like if there is any possibility of throwing before full initialization, deinit will not be called. Is that the case?

8 Likes

No, it doesn't call the deinitializer because the error thrown causes the program to cease execution

Misunderstood the question!

Based on experimentation, it looks like deinit will not be called until the first phase of the initialization process completes, that is, until all stored properties have been initialized and any super initializers have been called.

If you think about it this is necessary to ensure that you actually have a valid object in deinit. If deinit were called when you threw "beforeErr", doing something like printing self.x in the deinit would be an unsafe operation.

The throwing code is wrapped in a do/catch, so execution is not stopping based on the error. Also, even if it were stopping execution, why would deinit only be called in the second case (when there is no possibility of the "before error" being thrown) but not the first? The error comes from exactly the same place in both examples above, the only difference is whether the line where an earlier error may occur is commented out.

That was my understanding too, but I think it's not right; wouldn't you agree that the first phase is already complete when the "afterErr" is thrown, since I've set self.x?

My question here is really, why does the presence of a conditional at the start of the init method (a conditional which is never actually being executed and is not the source of the thrown error) affect whether deinit is called when we throw after the first phase completes?
That conditional being:

        if 1 == 2 {
            throw Err(message: "beforeError")
        }
1 Like

Ah! I missed the point that the conditional was always false :man_facepalming:. That is interesting behavior that I don't have an explanation for off the top of my head...

1 Like

Before this thread, I would have expected throwing in an init to never run the current class’s deinit, because initialization hasn’t completed yet, but now I have no idea.

I would have also expected that throwing after calling super to run super’s deinit, however, as it has been initialized.

All very strange. I guess for now the safest approach is to probably have a separate cleanup method and ensure that is called any time an error is being thrown from init, since the behavior here doesn't seem particularly reliable.

1 Like

I think this warrants a bug report - at the very least, whatever is supposed to happen should be clearly documented somewhere.

2 Likes

I've filed SR-13355.

3 Likes