What state is an object in while its deinit runs, and what happens if you let self escape from it?

3.8.1 · L3 · Interview

Predict first

What does this print, and what happens at the end?

final class Probe {
    weak var me: Probe?
    deinit { print("deinit runs; weak reference to myself is", me == nil ? "nil" : "alive") }
}
func a() { let p = Probe(); p.me = p }
a()

final class Escaper: Sendable {
    deinit {
        print("deinit runs; trying to hand self out")
        Registry.save(self)
    }
}
enum Registry { nonisolated(unsafe) static var saved: Escaper?; static func save(_ e: Escaper) { saved = e } }
func b() { _ = Escaper() }
b()
print("not reached")
  • It prints both deinit lines and “not reached”
  • The weak reference is alive in deinit, and the second deinit stores self successfully
  • The first deinit sees nil; the second deinit prints its line and the process stops with a runtime error, so “not reached” never prints

Mental model. A deinit is the last day at the job: you can hand back the keys and clear your desk, but you cannot decide to stay and keep your badge.

By the time deinit starts the strong count has already hit zero and the object is committed to dying. Weak references to it already read nil, even from inside its own deinit (the check prints nil). Its stored properties are still intact, so you can read them, call methods and release resources; the properties are destroyed after the body returns, and a superclass deinit runs after the subclass one (Q503). What you must not do is retain self beyond the end of deinit: storing it in a global, capturing it in an escaping closure or handing it to a queue. The runtime checks that the count is zero when the body finishes and, if not, stops the process with “deallocated with non-zero retain count … may have created a strong reference to self which outlived deinit”. Swift has no resurrection, unlike a garbage-collected finaliser.

The practical rules: keep deinit short and synchronous, release things you own (invalidate a timer, close a file, remove an observer) and never start work that needs self later. Calling a method that happens to capture self in a closure that outlives the call is the usual accidental escape, and it only crashes on the path where the object really dies there, so it can hide for months. Also remember deinit can run on any thread, namely whichever thread dropped the last reference (a classic source of UI-from-background-thread bugs). Isolated deinit (Swift 6.2) addresses that for actors and global-actor classes.

Weak references to a dying object read nil, and the runtime traps if a strong reference to self survives the deinit.

Common traps

  • Registering self somewhere from deinit. A notification centre that stores it strongly, a dispatch block that captures it: both outlive the object and trap.
  • Assuming deinit runs on the main thread because the class is a view model. It runs on the releasing thread unless you used isolated deinit.

They might follow up with

  • Can I use weak self inside deinit? It reads nil; read the stored properties directly instead.
  • What about unowned references to the dying object? They trap if read after the strong count reached zero (Q137).

Related questions

  • 3.8.2 Does deinit run when the app exits, and which objects never get one?
  • 3.8.3 Which thread does deinit run on, and what does isolated deinit change for actors and main-actor classes?
  • 3.2.5 What happens to an object’s memory while unowned references to it still exist?
  • 3.4.5 How does a weak reference get zeroed when the object dies?
  • 27.1.1 In what order do initialisers and deinitialisers run for a subclass with stored properties, and what does…

Does deinit run when the app exits, and which objects never get one? →