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?

7 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.

My understanding is that after all fields have been initialised, the object begins to reference-count (or rather, its reference counter is initialised alongside all the fields) — this is necessary if you want to start passing the reference to this object already from within the init.

Now, in your case, the only reference to the object (the one stored in c) gets destroyed because the scope is left immediately on throwing, so the reference count decrements to 0 and the deinit needs to be called, but strictly speaking, this happens for a reason fully independent from the thrown error.

If you consider this:

struct Err: Error {
    let message: String
}

var a: MyClass!

class MyClass {
    let x: Int
    let text: String

    init() throws {
        
        if false {
            throw Err(message: "beforeError")
        }
        
        self.x = 1234
        self.text = "Some text"
        
        a = self
        
        throw Err(message: "afterErr")
    }

    deinit {
        print("in deinit")
    }
}

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

print(a.x)
print(a.text)

— i.e., we manage to set a reference to the object being constructed before an error thrown in the initialiser, the deinit will not be called, because the reference count never reaches zero.

What's interesting, though, is that now we seem to get into some undefined behaviour territory, since the memory of a does not get initialised fully and the last two statements print garbage values (and occasionally traps on bad memory access). I'm not sure if that's has been already filed as a bug, though.

By the way, the same thing seems to happen with failable initialisers:

var b: MyOtherClass!
class MyOtherClass {
    
    var x: Int
  
    init?() {

        x = 1234
        b = self
        
        return nil
    }
    
    deinit {
        print("MyOtherClass deinit")
    }
}

let d = MyOtherClass()
print(b.x)

— apparently, having a failable initialiser does not at all mean that initialisation will fail :crazy_face: — rather, this only really means that init?() returns a nil pointer. Depending on whether you comment out the b = self line or not, you can get the deinit to be called even if you always return nil from the initialiser.

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
Terms of Service

Privacy Policy

Cookie Policy