Swift Deinitializers: How deinit Works and When It Runs

Swift deinitializers let a class instance clean up just before it is removed from memory. If you are learning classes and memory management, understanding deinit helps you write safer code, release resources at the right time, and diagnose bugs such as objects that never seem to go away.

Quick answer: A Swift deinitializer is the deinit block inside a class. It runs automatically when the last strong reference to that class instance is removed, and it is mainly used to perform final cleanup such as stopping observers, closing resources, or logging object destruction.

Difficulty: Beginner

Helpful to know first: You'll understand this better if you know basic Swift class syntax, how properties work, and that Swift uses Automatic Reference Counting (ARC) to manage class instances.

1. What Is a Deinitializer?

A deinitializer is special code that belongs to a class and runs automatically when the instance is about to be removed from memory.

  • It is written with the keyword deinit.
  • Only classes can have deinitializers.
  • You do not call a deinitializer yourself.
  • It takes no parameters and has no return value.
  • It is commonly used for final cleanup work.

In Swift, class instances are reference types. That means multiple variables can point to the same object. Swift uses ARC to keep track of how many strong references exist. When that count reaches zero, Swift destroys the instance and runs its deinitializer.

A deinitializer is closely related to an initializer, but they are not opposites in every detail. An initializer sets an object up when it is created. A deinitializer tears it down when it is no longer needed. Unlike init, a class can have only one deinit.

2. Why Deinitializers Matter

Many class instances do not need a custom deinitializer at all, because ARC automatically frees ordinary stored properties. However, some objects manage work or resources that should be stopped or released explicitly.

Deinitializers matter because they help you:

  • remove observers before the object disappears
  • invalidate timers or ongoing tasks
  • close files or network-related helpers owned by the object
  • release custom unmanaged resources
  • debug object lifetime by printing when an instance is destroyed

Without proper cleanup, an object may leave behind extra work, unexpected behavior, or hidden memory problems. For example, if a class starts a timer and never invalidates it, the timer may continue running longer than expected.

A deinitializer is for cleanup, not for normal business logic. If your program depends on deinit to perform everyday application work, the design is usually too fragile.

3. Basic Syntax

Simple deinitializer syntax

The basic syntax is short. You place a deinit block inside a class definition.

Swift
class FileSession {
    let name: String

    init(name: String) {
        self.name = name
        print("Opened \(name)")
    }

    deinit {
        print("Closed \(name)")
    }
}

This class prints a message when it is created and another message when it is destroyed. The deinitializer has no parentheses because it cannot accept arguments.

When it runs

Here is a minimal example showing object lifetime.

Swift
class UserSession {
    let username: String

    init(username: String) {
        self.username = username
        print("Session started for \(username)")
    }

    deinit {
        print("Session ended for \(username)")
    }
}

var session: UserSession? = UserSession(username: "Taylor")
session = nil
Output✓ Tested · Swift 6.4
Session started for Taylor
Session ended for Taylor

When session is set to nil, the last strong reference is removed, so the object is deallocated and deinit runs.

deinit vs init

The two are related but used differently.

Swift
class Counter {
    var value = 0

    init() {
        print("Counter created")
    }

    deinit {
        print("Counter destroyed")
    }
}

init prepares the object for use. deinit performs final cleanup after the object is no longer needed.

4. Step-by-Step Examples

Example 1: Watching a simple object lifecycle

This first example shows the easiest way to see a deinitializer in action.

Swift
class Message {
    let text: String

    init(text: String) {
        self.text = text
        print("Created: \(text)")
    }

    deinit {
        print("Destroyed: \(text)")
    }
}

var message: Message? = Message(text: "Hello")
message = nil
Output✓ Tested · Swift 6.4
Created: Hello
Destroyed: Hello

The output shows creation first and destruction second. This helps beginners understand that deinit is tied to the instance lifecycle.

Example 2: Multiple references to the same object

A deinitializer does not run until the last strong reference is gone.

Swift
class Document {
    let title: String

    init(title: String) {
        self.title = title
    }

    deinit {
        print("Document removed: \(title)")
    }
}

var doc1: Document? = Document(title: "Report")
var doc2 = doc1

doc1 = nil
print("doc1 cleared")

doc2 = nil
print("doc2 cleared")
Output✓ Tested · Swift 6.4
doc1 cleared
Document removed: Report
doc2 cleared

After doc1 = nil, the instance still exists because doc2 still references it. Only after doc2 = nil does the deinitializer run.

Example 3: Cleaning up helper work

This example simulates an object that starts work and must stop it before being destroyed.

Swift
class DownloadTask {
    var isRunning = false

    init() {
        isRunning = true
        print("Download started")
    }

    func stop() {
        if isRunning {
            isRunning = false
            print("Download stopped")
        }
    }

    deinit {
        stop()
        print("DownloadTask destroyed")
    }
}

var task: DownloadTask? = DownloadTask()
task = nil
Output✓ Tested · Swift 6.4
Download started
Download stopped
DownloadTask destroyed

Here the deinitializer acts as a final safety net. If the task is still running when the object disappears, cleanup still happens.

Example 4: Breaking a strong reference chain with weak

One common reason a deinitializer does not run is a retain cycle. This example shows the correct use of a weak reference.

Swift
class Owner {
    let name: String
    var device: Device?

    init(name: String) {
        self.name = name
    }

    deinit {
        print("Owner destroyed: \(name)")
    }
}

class Device {
    let model: String
    weak var owner: Owner?

    init(model: String) {
        self.model = model
    }

    deinit {
        print("Device destroyed: \(model)")
    }
}

var owner: Owner? = Owner(name: "Chris")
var device: Device? = Device(model: "Tablet")

owner?.device = device
device?.owner = owner

owner = nil
device = nil
Output✓ Tested · Swift 6.4
Owner destroyed: Chris
Device destroyed: Tablet

Because owner inside Device is weak, both objects can be released normally. If both references were strong, neither object would deinitialize.

5. Practical Use Cases

Deinitializers are most useful in classes that own something needing explicit shutdown or cleanup.

  • Stopping a custom worker object when a screen controller or manager object goes away
  • Removing notifications or observation tokens owned by a class instance
  • Invalidating a timer that should not continue after the object is gone
  • Closing a file handle wrapper or similar resource-owning helper
  • Logging object destruction while debugging memory leaks
  • Releasing unmanaged or low-level resources wrapped by a Swift class

If a class only stores ordinary Swift values and references that ARC can manage automatically, you may not need a deinitializer at all.

6. Common Mistakes

Mistake 1: Expecting deinit to exist in a struct

Deinitializers belong to classes, not structures or enumerations. Beginners sometimes try to use deinit with a value type because they want cleanup behavior everywhere.

Problem: This code uses deinit in a struct, which Swift does not allow because structs are value types and are not managed by ARC in the same way as classes.

Swift
struct FileRecord {
    let name: String

    deinit {
        print("Cleaning up \(name)")
    }
}
Compiler output✓ Tested · Swift 6.4
error: deinitializer cannot be declared in struct 'FileRecord' that conforms to 'Copyable'

Fix: Use a class if you need object lifetime behavior tied to ARC and deinit.

Swift
class FileRecord {
    let name: String

    init(name: String) {
        self.name = name
    }

    deinit {
        print("Cleaning up \(name)")
    }
}

The corrected version works because classes are reference types and support deinitializers.

Mistake 2: Assuming deinit runs as soon as one variable becomes nil

If an object has multiple strong references, clearing only one of them is not enough. The instance stays alive until the last strong reference disappears.

Problem: This code assumes setting one variable to nil destroys the object immediately, but another strong reference still keeps it alive.

Swift
class Account {
    deinit {
        print("Account destroyed")
    }
}

var firstRef: Account? = Account()
var secondRef = firstRef

firstRef = nil

Fix: Remove every strong reference before expecting the object to deinitialize.

Swift
class Account {
    deinit {
        print("Account destroyed")
    }
}

var firstRef: Account? = Account()
var secondRef = firstRef

firstRef = nil
secondRef = nil
Output✓ Tested · Swift 6.4
Account destroyed

The corrected version works because ARC can finally reduce the strong reference count to zero.

Mistake 3: Creating a retain cycle so deinit never runs

Two class instances can keep each other alive if both store strong references. This is one of the most common reasons developers think deinit is "not working".

Problem: Both objects strongly reference each other, so ARC cannot reduce either one to zero references. Their deinitializers never run.

Swift
class Customer {
    var card: CreditCard?
    deinit {
        print("Customer destroyed")
    }
}

class CreditCard {
    var customer: Customer?
    deinit {
        print("CreditCard destroyed")
    }
}

Fix: Make one side weak or unowned, depending on the ownership relationship.

Swift
class Customer {
    var card: CreditCard?
    deinit {
        print("Customer destroyed")
    }
}

class CreditCard {
    weak var customer: Customer?
    deinit {
        print("CreditCard destroyed")
    }
}

The corrected version works because the weak reference does not increase the strong reference count.

Mistake 4: Trying to call deinit manually

A deinitializer is controlled by ARC, not by your own code. You cannot invoke it like a normal method.

Problem: This code treats deinit like a function, but Swift does not allow manual calls to a deinitializer.

Swift
class Logger {
    deinit {
        print("Logger destroyed")
    }
}

let logger = Logger()
// logger.deinit()

Fix: Remove the strong references instead, or create a separate cleanup method if you need explicit manual shutdown before deallocation.

Swift
class Logger {
    func close() {
        print("Logger closed manually")
    }

    deinit {
        print("Logger destroyed")
    }
}

var logger: Logger? = Logger()
logger?.close()
logger = nil
Output✓ Tested · Swift 6.4
Logger closed manually
Logger destroyed

The corrected version works because manual cleanup and automatic deallocation are handled separately.

7. Best Practices

Use deinit for cleanup, not primary program flow

It is fine to release resources or stop work in a deinitializer, but your app should not depend on deinit to trigger essential user-facing behavior at a precise moment.

Swift
class Uploader {
    func finishUpload() {
        print("Upload finished")
    }

    deinit {
        print("Uploader destroyed")
    }
}

The preferred pattern is to use a normal method for explicit business actions and reserve deinit for final cleanup.

Keep deinitializers short and predictable

A deinitializer should usually perform quick cleanup tasks. Very complex work inside deinit can make object lifetimes harder to reason about.

Swift
class ObserverBox {
    var isObserving = true

    func stopObserving() {
        if isObserving {
            isObserving = false
            print("Observation removed")
        }
    }

    deinit {
        stopObserving()
    }
}

This is easier to understand than packing a lot of unrelated logic directly into the deinitializer.

Use weak or unowned references deliberately

If two objects reference each other, think carefully about ownership. One side often should not strongly own the other.

Swift
class Department {
    var manager: Manager?
}

class Manager {
    weak var department: Department?
}

This practice prevents retain cycles and allows deinitializers to run when the objects are no longer needed.

8. Limitations and Edge Cases

  • Only classes can define deinit. Structs and enums cannot.
  • A class can have only one deinitializer.
  • You cannot call deinit directly.
  • The exact moment of deinitialization depends on ARC and remaining strong references.
  • If a retain cycle exists, deinit may never run until the cycle is broken.
  • Stored properties are released automatically, so you do not need deinitializers for ordinary memory cleanup alone.
  • If a class inherits from another class, the subclass deinitializer runs before the superclass is deinitialized as the instance is torn down.
  • A common "deinit not working" situation is actually a hidden strong reference, such as a closure or another object still holding the instance.

When debugging lifecycle issues, adding a simple print inside deinit is a practical way to see whether an object is actually being released.

9. Practical Mini Project

This mini project creates a small class that simulates a connection. It starts active, can be closed manually, and also guarantees cleanup in its deinitializer.

Swift
class Connection {
    let id: Int
    private var isOpen = false

    init(id: Int) {
        self.id = id
        isOpen = true
        print("Connection \(id) opened")
    }

    func send(message: String) {
        if isOpen {
            print("Sending: \(message)")
        } else {
            print("Cannot send, connection is closed")
        }
    }

    func close() {
        if isOpen {
            isOpen = false
            print("Connection \(id) closed manually")
        }
    }

    deinit {
        if isOpen {
            print("Connection \(id) closed in deinit")
        }
        print("Connection \(id) destroyed")
    }
}

var connection: Connection? = Connection(id: 101)
connection?.send(message: "Hello server")
connection?.close()
connection = nil
Output✓ Tested · Swift 6.4
Connection 101 opened
Sending: Hello server
Connection 101 closed manually
Connection 101 destroyed

This example shows a clean design: explicit manual closing is available through close(), and deinit acts as a backup cleanup point when the object is finally released.

10. Key Points

  • deinit is a class-only feature used for final cleanup before an instance is removed from memory.
  • Swift runs a deinitializer automatically when the last strong reference to the instance is gone.
  • You cannot call a deinitializer yourself, and it does not take parameters.
  • Most ordinary memory cleanup is handled by ARC automatically.
  • Deinitializers are especially useful for stopping work, removing observers, or releasing owned resources.
  • If deinit does not run, a hidden strong reference or retain cycle is often the cause.
  • weak and unowned references help prevent cycles that block deinitialization.

11. Practice Exercise

Try building a class that tracks a temporary editing session.

  • Create a class named EditingSession with a documentName property.
  • Print a message when the session starts.
  • Add a method named endSession() that prints a manual closing message.
  • Add a deinit that prints a destruction message.
  • Create an optional instance, call endSession(), then set the variable to nil.

Expected output: You should see a start message, then a manual end message, then a deinitialization message.

Hint: Use a class, not a struct, and make the variable optional so you can set it to nil.

Show solution
Swift
class EditingSession {
    let documentName: String

    init(documentName: String) {
        self.documentName = documentName
        print("Started editing \(documentName)")
    }

    func endSession() {
        print("Ended session for \(documentName)")
    }

    deinit {
        print("EditingSession destroyed for \(documentName)")
    }
}

var session: EditingSession? = EditingSession(documentName: "Notes.txt")
session?.endSession()
session = nil
Output✓ Tested · Swift 6.4
Started editing Notes.txt
Ended session for Notes.txt
EditingSession destroyed for Notes.txt

12. Final Summary

Swift deinitializers are a small feature, but they teach an important part of how classes behave. A deinit block runs automatically when the last strong reference to a class instance disappears, which makes it the right place for final cleanup work. In everyday Swift code, that often means stopping background work, removing observers, invalidating timers, or logging object destruction while debugging.

The most important practical idea is that deinitializers depend on ARC. If an object still has a strong reference somewhere, it will not be destroyed yet. If a retain cycle exists, deinit may never run at all. Once you understand that relationship between deinit, strong references, and weak references, you can reason much more confidently about class lifetimes in Swift.

A good next step is to study Swift Automatic Reference Counting in more depth, especially strong, weak, and unowned references. That will make deinitializers feel much more predictable in real projects.