DEV Community

Gamya
Gamya

Posted on

Swift Classes โ€” Initializers and the super.init() Rule ๐Ÿ—๏ธ

Logic behind two-phase initialization

Class initializers in Swift come with one golden rule that catches everyone out the first time. Let's talk about what it is, why it exists, and how to stop forgetting it.

Okay so we need to talk about initializers again. I know, I know โ€” we covered them in the structs chapter. But classes have a twist that trips people up the first time they hit it, and it's worth slowing down on before you run into it at 11pm debugging something that seems like it should be obvious.

Here's the short version of what we're going to learn today:

When a child class has its own initializer, it must call the parent's initializer โ€” and it has to do it after setting up its own stuff first.

That's it. That's the rule. But let's actually understand why, because "just do it" is a terrible way to remember something. ๐Ÿฅ


Setting the Scene

Let's build a ninja academy system. Every person in the ninja world has a chakraType โ€” whether they use fire, wind, water, earth, or lightning chakra. That's a property that belongs to everyone, so it lives in the parent class:

class NinjaAcademy {
    let chakraType: String

    init(chakraType: String) {
        self.chakraType = chakraType
    }
}
Enter fullscreen mode Exit fullscreen mode

Simple enough. Now let's say we want a StudentNinja that inherits from NinjaAcademy but also has a rank โ€” like Genin, Chunin, or Jonin:

class StudentNinja: NinjaAcademy {
    let rank: String

    init(rank: String) {
        self.rank = rank
    }
}
Enter fullscreen mode Exit fullscreen mode

This looks reasonable. But Swift will refuse to build it. And the error message will say something like: "Super.init isn't called on all paths before returning from initializer."


Why Swift is Refusing

Here's the problem. NinjaAcademy has a property called chakraType. It's required โ€” there's no default value, and Swift's golden rule for initializers is every property must have a value by the time initialization is done.

When StudentNinja is created, it inherits chakraType from NinjaAcademy. But who sets it? StudentNinja's initializer doesn't mention it. And if nobody sets it, chakraType ends up without a value โ€” which Swift will not allow.

The solution is to accept chakraType in StudentNinja's initializer too, and then pass it up to NinjaAcademy using super.init():

class StudentNinja: NinjaAcademy {
    let rank: String

    init(chakraType: String, rank: String) {
        self.rank = rank
        super.init(chakraType: chakraType)
    }
}
Enter fullscreen mode Exit fullscreen mode

Now it works. Let's break down what's happening on those two critical lines.


The Order Matters: Your Stuff First, Then Super

Notice the order:

self.rank = rank           // 1. Set your own properties first
super.init(chakraType: chakraType) // 2. Then call the parent's initializer
Enter fullscreen mode Exit fullscreen mode

This order is not optional. Swift enforces it. If you try to call super.init() before setting self.rank, Swift will refuse:

// โŒ This won't work โ€” wrong order
init(chakraType: String, rank: String) {
    super.init(chakraType: chakraType)
    self.rank = rank // too late โ€” super.init already ran
}
Enter fullscreen mode Exit fullscreen mode

The reason is safety. Swift wants to make sure the current class's own properties are fully set up before it hands control to the parent class. If something went wrong during super.init(), you'd at least know your own class's properties were in a valid state.

Think of it like building a house. You have to finish your own floor before you can ask the architect to come check the whole building. The architect (super.init) needs to know the floor they're standing on is solid.


Using the Finished Class

With the initializers in place, you can now create a StudentNinja like this:

let sakura = StudentNinja(chakraType: "Fire", rank: "Genin")
print(sakura.chakraType) // "Fire" โ€” inherited from NinjaAcademy
print(sakura.rank)       // "Genin" โ€” defined in StudentNinja
Enter fullscreen mode Exit fullscreen mode

Both properties are accessible, both are set correctly. sakura is a StudentNinja, but she also has everything NinjaAcademy defined because that's what inheritance gives you.


What If the Subclass Has No Custom Initializer?

Here's the good news: if your subclass doesn't add any new properties that need setting, you don't need to write an initializer at all. Swift will just inherit the parent's automatically:

class EliteNinja: NinjaAcademy {
    func specialMove() {
        print("Using \(chakraType) chakra for a special attack!")
    }
}

let kakashi = EliteNinja(chakraType: "Lightning")
kakashi.specialMove() // "Using Lightning chakra for a special attack!"
Enter fullscreen mode Exit fullscreen mode

EliteNinja adds a method but no new properties โ€” so it inherits NinjaAcademy's initializer for free and you don't have to write anything. More precisely: the super.init() requirement kicks in whenever your subclass defines its own designated initializer โ€” even if it adds no new properties. The common case is adding properties (like rank above), but the rule is really about the initializer chain, not the properties themselves.


super Is Not Just for Initializers

While we're here โ€” super isn't limited to init. You can use it to call any method from the parent class, which is useful when you're overriding a method but still want to run the parent's version too.

For this to work, the method needs to exist on the parent class first. Let's add one to NinjaAcademy:

class NinjaAcademy {
    let chakraType: String

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

    func train() {
        print("Training with \(chakraType) chakra...")
    }
}
Enter fullscreen mode Exit fullscreen mode

Now Hokage can override it and call the parent's version using super:

class Hokage: NinjaAcademy {
    let title: String

    init(chakraType: String, title: String) {
        self.title = title
        super.init(chakraType: chakraType)
    }

    override func train() {
        super.train() // runs NinjaAcademy's version first
        print("\(title) also reviews mission reports after training.")
    }
}

let naruto = Hokage(chakraType: "Wind", title: "Seventh Hokage")
naruto.train()
// "Training with Wind chakra..."
// "Seventh Hokage also reviews mission reports after training."
Enter fullscreen mode Exit fullscreen mode

Same pattern: super gives you access to the parent, whether you're in an initializer or a regular method.


The One Thing To Hold Onto

The rule that catches everyone: when your child class has its own initializer, set your own properties first, then call super.init().

Get that order right and everything works. Get it backwards and Swift will stop you immediately. It feels pedantic until you understand why โ€” Swift is making sure every single property has a value before initialization completes, no matter how deep the inheritance chain goes. That's not pedantry, that's safety. ๐ŸŒธ


This article was written by me; AI was used to improve grammar and readability.


Top comments (6)

Collapse
 
merbayerp profile image
Mustafa ERBAY

This is a clear beginner-friendly explanation of Swiftโ€™s initialization order, and the StudentNinja example demonstrates the first phase of class initialization nicely.

There are two details worth correcting, though. The requirement to delegate to a superclass initializer is not triggered specifically by adding new stored properties. It comes from Swiftโ€™s designated-initializer delegation rules. A subclass that defines its own designated initializer still has to complete the superclass initialization chain even when it introduces no new properties, except in the limited case where Swift can insert an implicit call to a synchronous zero-argument super.init().

The Hokage example also needs a method such as someMethod() to exist in NinjaAcademy; otherwise override func someMethod() and super.someMethod() will not compile.

I would also frame the ordering in terms of Swiftโ€™s two-phase initialization model: subclass properties are initialized before delegation upward, and only after phase one completes can the instance be customized more freely on the way back down. That is slightly more precise than saying the properties are initialized first in case super.init() fails.

Collapse
 
gamya_m profile image
Gamya

Thank you for these corrections โ€” genuinely appreciate the precision here! ๐Ÿ˜Š

You're right on the designated initializer point โ€” the way I framed it tied the super.init() requirement specifically to adding new stored properties, which is an oversimplification. The delegation rule applies whenever a subclass defines its own designated initializer, regardless of whether new properties were introduced. That's a meaningful distinction and I should have been more careful with it.

The Hokage someMethod() example is a fair catch too โ€” as written it wouldn't compile since the method doesn't exist on the parent class. That was a careless illustration on my part.

The two-phase initialization framing is also more accurate than what I wrote. "Set your stuff first, then call super" is a useful memory aid for beginners but it doesn't capture why the order matters โ€” phase one completes first to ensure the whole instance is in a valid state before phase two allows customization. The current framing implies the concern is about super.init() potentially failing, when the real model is about what the instance is allowed to do at each phase.

I'll carry these corrections forwardโ€”this is exactly the kind of feedback that makes the series better.

Collapse
 
merbayerp profile image
Mustafa ERBAY

Glad it was helpful! I think your beginner-friendly explanation is still valuableโ€”the corrections are mostly about making the mental model line up with Swiftโ€™s actual initialization rules. Looking forward to the next part of the series!

Thread Thread
 
gamya_m profile image
Gamya

Really appreciate that โ€” and yes, that's exactly the balance I'm trying to strike, keeping it approachable without the simplification accidentally teaching something wrong. Thanks for helping get it right! The next part is already in the pipeline

Collapse
 
gamya_m profile image
Gamya

Thanks again for catching thoseโ€”I've updated the article with the corrections. The super.init() explanation now reflects the designated initializer chain rule more accurately, and the train() method is properly defined on NinjaAcademy before being overridden in Hokage. Really appreciate the careful read! ๐ŸŒธ

Collapse
 
buildbasekit profile image
buildbasekit

Swift compiler: "You forgot super.init()." Me: "I was hoping you wouldn't notice." ๐Ÿ˜… Nice explanation!