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
}
}
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
}
}
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)
}
}
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
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
}
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
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!"
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...")
}
}
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."
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)
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.
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.
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!
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
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! ๐ธ
Swift compiler: "You forgot super.init()." Me: "I was hoping you wouldn't notice." ๐ Nice explanation!