Designated Initializers, Convenience Initializers, and Initializer Chaining
Explain the designated vs. convenience initializer pattern, correctly chain initialization up to a superclass with self = [super init], implement failable initialization by returning nil, use instancetype correctly, and distinguish self from super.
The previous lesson established that init sets up an object's real state, but a class often needs more than one way to create an object — sometimes with every detail specified, sometimes with sensible defaults filled in automatically. This lesson covers the full initializer toolkit Objective-C classes actually use in practice: the designated initializer every other initializer funnels through, convenience initializers that wrap it, the self = [super init] chaining pattern that connects a class to its superclass's own setup, failable initialization for when construction can legitimately fail, and the self/super/instancetype vocabulary that makes all of it work correctly across subclasses.
The Designated Initializer: One Real Path Through Setup
A class can have several different initializers, but well-designed classes funnel all of them through exactly one — the designated initializer — which is the single method that actually performs the complete setup: assigning every property, running any validation, doing whatever full initialization genuinely requires. Every other initializer the class offers, no matter how it's called, ultimately calls the designated initializer internally rather than duplicating its setup logic.
For a Person class, a natural designated initializer might be - (instancetype)initWithName:(NSString *)name age:(NSInteger)age, taking every piece of information the object actually needs. This is 'the real one' — the place where self.name = name; and self.age = age; (and any future validation, like rejecting a negative age) actually happens. Having exactly one place where full setup occurs means that if you later need to change how initialization works — add a new required property, add validation logic — there is exactly one method to change, and every other initializer automatically benefits because they all call through it.
This matters enormously for maintainability: without a single designated initializer, a class with multiple ad hoc initializers risks each one independently (and inconsistently) setting up the object's state, so a bug fix or new validation rule added to one initializer silently fails to apply to the others. Funneling everything through one designated initializer eliminates that entire category of bug by construction.
• A class may offer several initializers, but should funnel all of them through exactly one designated initializer. • The designated initializer performs the actual complete setup: assigning every property, running validation. • Example: - (instancetype)initWithName:(NSString *)name age:(NSInteger)age as Person's designated initializer. • One designated initializer means one place to fix bugs or add validation, automatically benefiting every other initializer.
Convenience Initializers: Easier to Call, Same Underlying Setup
A convenience initializer is a simpler-to-call initializer that internally calls the designated initializer, supplying sensible default values for whatever it doesn't ask the caller for. Rather than duplicating setup logic, a convenience initializer's entire job is to gather or default some values and then hand off to the designated initializer to do the real work — it should contain little to no setup logic of its own.
For example, + (instancetype)personWithName:(NSString *)name could be a convenience initializer that creates a Person given only a name, defaulting the age to some reasonable starting value like 0, by internally calling [[Person alloc] initWithName:name age:0] (or an equivalent instance-side convenience initializer written as -(instancetype)initWithName:(NSString *)name that calls [self initWithName:name age:0]). Note this one is written as a class method (the leading +) rather than an instance method (-) — class-method convenience initializers, often named +classNameWithSomething:, are extremely common in Foundation and UIKit (think [NSArray arrayWithObject:x] or [UIColor colorWithRed:g:b:alpha:]), and they handle alloc internally so the caller doesn't have to write [[Person alloc] ...] at all.
The value of this pattern is that callers who don't care about every detail get a shorter, simpler call, while the class still guarantees that full, correct setup always happens somewhere — because the convenience initializer's default-filling always ends by calling the one designated initializer, never by duplicating its logic.
• A convenience initializer is simpler to call and internally calls the designated initializer with default values filled in. • It should contain little to no setup logic of its own -- its job is to gather/default values, then hand off. • Example: +(instancetype)personWithName:(NSString *)name defaults age and calls the designated initializer internally. • Class-method convenience initializers (+classNameWithSomething:) are common throughout Foundation and UIKit, and typically handle alloc internally.
Initializer Chaining: self = [super init]
Every Objective-C class (other than the root, NSObject) has a superclass, and that superclass has its own initialization needs — its own properties and setup logic that must run before a subclass's own initialization makes sense to perform. Initializer chaining is how this gets guaranteed: at the very start of any initializer, before doing anything else, you call up to the superclass's own initializer and capture its result: self = [super init]; — always call up to the superclass first, before touching any of the current class's own properties.
The reason this line looks the way it does — reassigning self, not just calling [super init] as a bare statement — connects directly back to the previous lesson's point that init is not guaranteed to return the same object it was called on. [super init] might, in principle, return a different object than the one you started with (or nil, in a failable initializer, covered next), so the pattern always reassigns self to whatever the superclass's initializer actually hands back, and every subsequent line in your initializer should use that updated self, not any earlier reference.
The full pattern, in Person's designated initializer, looks like: self = [super init]; if (self) { self.name = name; self.age = age; } return self; — call up to super first, check that the result isn't nil (a failable initializer might return nil, covered next), only then set up this class's own properties, and finally return self. Skipping the if (self) check and setting properties unconditionally is a real, common mistake: if [super init] returned nil, you'd be sending messages to nil (which is harmless in Objective-C — it silently does nothing — but means your object never actually got the state you thought it had), and returning a non-nil self from an initializer that should have failed.
• self = [super init]; -- always call up to the superclass's initializer first, and capture whatever it returns. • Reassigning self (not just calling super init as a bare statement) matters because super's init might return a different object, or nil. • Standard pattern: self = [super init]; if (self) { /* set up this class's own properties */ } return self; • Skipping the if (self) check risks silently proceeding with a nil self after a failed superclass initialization.
Failable Initialization: Returning nil
Not every attempt to construct an object can succeed. If an initializer is given invalid input — a negative age, a malformed URL string, a file path that doesn't exist — the right response is often to fail construction entirely rather than produce a half-valid object. Objective-C supports this directly: an initializer can return nil instead of self, signaling 'this object could not be created' to the caller.
A failable version of Person's initializer might look like: self = [super init]; if (self) { if (age < 0) { return nil; } self.name = name; self.age = age; } return self; — after successfully chaining to super, the initializer validates its input and returns nil immediately if the age is invalid, before ever assigning it. The object that alloc originally created still technically existed in memory for a moment, but nil is what gets handed back to the caller, and Objective-C's memory management cleans up the abandoned partially-initialized object automatically.
Because initialization can fail this way, callers must always check the result of a call to init (or any initializer) before using it: Person *person = [[Person alloc] initWithName:@"Sam" age:-5]; if (person != nil) { /* safe to use */ } else { /* handle the failure */ }. Skipping this check and using person unconditionally is a real hazard — sending messages to a nil object in Objective-C doesn't crash (it silently does nothing and returns a zero-equivalent value), which means a bug from an unchecked failed initialization often doesn't announce itself with a crash; it just quietly produces wrong, missing behavior that's much harder to trace back to its actual cause.
• An initializer can return nil to signal that construction failed, instead of returning self. • Validate input after successfully chaining to super, and return nil before assigning any invalid state. • Callers must always check an initializer's result for nil before using the object. • Sending messages to nil in Objective-C silently does nothing rather than crashing -- so an unchecked failed init often produces a quiet, hard-to-trace bug rather than an obvious one.
instancetype: The Correct Return Type for Initializers
instancetype is a special return type meaning, in effect, 'whatever type this method was actually called on' -- and it is the return type every initializer should declare, in preference to explicitly naming the class. Writing - (instancetype)initWithName:(NSString *)name age:(NSInteger)age tells the compiler: whatever class this initializer ends up being called through, the object it returns should be understood as being that same class.
This matters specifically because of subclassing. Suppose a Student class subclasses Person and inherits initWithName:age: without overriding it. If that method had instead been declared to return a literal Person *, then Student *s = [[Student alloc] initWithName:@"Sam" age:20]; would compile, but the compiler would believe s holds a Person, not a Student -- silently discarding useful type information and potentially warning about (or hiding real bugs behind) a mismatched type. Declaring the return type as instancetype instead means the compiler correctly infers Student in that exact same call, because instancetype tracks the actual calling class, not whatever class the method happened to be written in.
The practical rule is simple: always declare instancetype as the return type for init and any other initializer (including convenience initializers like the class-method personWithName: pattern from earlier), never the class's own literal name, and never id -- instancetype gives you both the correct type-checking id would sacrifice and the correct subclass-awareness a literal class name would sacrifice.
• instancetype means 'whatever type this method was actually called on' -- resolved per call site, not fixed to the class where the method is written. • Declare it as the return type for every initializer: -(instancetype)init, -(instancetype)initWithName:age:, +(instancetype)personWithName:. • Without instancetype (using a literal class name instead), a subclass calling an inherited initializer loses correct type information. • instancetype is strictly better than id here: it keeps full compiler type-checking while still being correct for subclasses.
self and super: Two Very Different Kinds of Receiver
self refers to the actual receiving instance -- inside any instance method, self is a reference to the particular object that message was sent to, letting that method read and modify its own object's properties and call its own other methods. Every property access shown throughout this course (self.name, self.age = age;) is really a message sent to self, the current object, exactly the same as any other message send.
super, by contrast, is not a separate object at all -- it's a special receiver-forwarding keyword. Sending a message to super means 'start looking up this method starting at my superclass's implementation, rather than at my own class's,' while the message is still, underneath, being sent to the exact same object self refers to. This is a subtle but important distinction: super doesn't mean 'a different object, my superclass's instance' (there is no separate superclass instance -- there's only ever the one object) -- it means 'search for this method's implementation starting one level up the class hierarchy, but run it on the same object.'
This is precisely why self = [super init] makes sense as a single line: [super init] sends the init message to the same underlying object self already refers to, but forces the lookup to begin at NSObject's (or whatever the immediate superclass is) implementation of init rather than the current class's own -- letting the superclass perform its own setup on the object first, before the subclass's own initializer continues setting up the rest. Every subclass's initializer needs this exact pattern, because skipping it would mean the superclass never gets a chance to set up whatever state it's responsible for.
• self = the actual receiving instance -- used constantly to access and modify the current object's own properties and methods. • super is not a separate object -- it's a keyword meaning 'look up this method starting at my superclass's implementation', still operating on the same object as self. • self = [super init] sends init to the current object, but starts the method lookup one level up the class hierarchy. • Every initializer needs to chain to super so the superclass gets a chance to perform its own setup on the object first.
Code Examples
The designated initializer always chains to super first (self = [super init]), checks the result isn't nil before touching any properties, then sets up this class's own state, and finally returns self -- the standard, complete initializer shape.
personWithName: is a convenience initializer: it takes fewer arguments, defaults age to 0, and hands off to the designated initializer (initWithName:age:) to actually perform the real setup -- it contains no setup logic of its own, only a default value and a handoff.
Passing a negative age causes the initializer to return nil before ever setting name or age. The caller's if (p != nil) check is mandatory -- skipping it and using p unconditionally would silently do nothing on every message sent to it, hiding the failure instead of surfacing it. Expected result: Failed to create Person: invalid age - (instancetype)initWithName:(NSString *)name age:(NSInteger)age { self.name = name; // never called [super init] first self.age = age; return self; } - (instancetype)initWithName:(NSString *)name age:(NSInteger)age { self = [super init]; if (self) { self.name = name; self.age = age; } return self; }
Forgetting self = [super init] skips the superclass's own setup entirely, and also skips capturing whatever object the superclass's initializer actually returns (which might differ from the original alloc result). Every initializer must chain to super before setting up its own state.
Person *p = [[Person alloc] initWithName:@"Sam" age:-5]; NSLog(@"%@", p.name); // used unconditionally, no nil check
Person *p = [[Person alloc] initWithName:@"Sam" age:-5]; if (p != nil) { NSLog(@"%@", p.name); } else { NSLog(@"Person creation failed"); }
Failing to check whether a failable initializer returned nil means a construction failure goes silently unnoticed -- sending messages to nil in Objective-C doesn't crash, it just does nothing, producing a quiet bug instead of an obvious error.
• (Person *)initWithName:(NSString *)name age:(NSInteger)age {... } - (instancetype)initWithName:(NSString *)name age:(NSInteger)age {... }
Declaring an initializer's return type as the literal class name instead of instancetype loses correct type information when the initializer is inherited and called through a subclass -- instancetype correctly resolves to whatever class the method was actually called on.
+ (instancetype)personWithName:(NSString *)name { Person *p = [[Person alloc] init]; p.name = name; p.age = 0; return p; // duplicates setup instead of calling the designated initializer }
+ (instancetype)personWithName:(NSString *)name { return [[Person alloc] initWithName:name age:0]; }
A convenience initializer should hand off to the designated initializer rather than duplicating its setup logic -- otherwise, any future validation or setup change made to the designated initializer silently fails to apply here too.
What is the role of a designated initializer in a class with multiple initializers? A. It is simply the first initializer written in the file, with no special role B. It is the single initializer that performs the complete, real setup; every other initializer funnels through it rather than duplicating its logic C. It is only used internally and can never be called directly D. It is required to have zero parameters
B. It is the single initializer that performs the complete, real setup; every other initializer funnels through it rather than duplicating its logic — The designated initializer is where full setup actually happens. Convenience initializers gather or default values and then call it, so there is exactly one place where the class's real initialization logic needs to be correct.
Why is the line written as self = [super init]; rather than just [super init]; on its own? A. Because [super init] on its own is a syntax error B. Because super's init call might return a different object than the one you started with (or nil), so self must be updated to whatever it actually returns C. Because self and super always refer to different objects D. Purely stylistic -- both forms behave identically
B. Because super's init call might return a different object than the one you started with (or nil), so self must be updated to whatever it actually returns — Just like any init call, [super init] is not guaranteed to return the exact same object it was sent to. Reassigning self ensures every subsequent line in the initializer operates on whatever object is actually correct.
Why should a caller always check whether an initializer's return value is nil? A. Because unchecked nil values cause an immediate crash in Objective-C B. Because a failable initializer can legitimately return nil to signal construction failed, and sending messages to nil silently does nothing rather than erroring -- so an unchecked failure produces a quiet, hard-to-trace bug C. Because nil checks are only needed for numeric properties D. It isn't necessary; Objective-C automatically retries a failed initializer
B. Because a failable initializer can legitimately return nil to signal construction failed, and sending messages to nil silently does nothing rather than erroring -- so an unchecked failure produces a quiet, hard-to-trace bug — Since sending a message to nil silently does nothing in Objective-C, skipping a nil check after a failable initializer hides the failure instead of surfacing it -- always verify the result before using it.
Why is instancetype preferred over declaring an initializer's return type as the literal class name? A. instancetype is required by the compiler for all methods, not just initializers B. instancetype resolves to whatever class the method was actually called on, so a subclass calling an inherited initializer gets correctly typed back, unlike a fixed class-name return type C. instancetype makes the code run faster at runtime D. There's no real difference -- it's purely a style preference
B. instancetype resolves to whatever class the method was actually called on, so a subclass calling an inherited initializer gets correctly typed back, unlike a fixed class-name return type — instancetype is call-site-aware: when a subclass calls an inherited initializer, instancetype correctly resolves to the subclass's type, whereas a literal class-name return type would incorrectly type the result as the superclass.
What does sending a message to super actually do? A. It sends the message to a separate object representing the superclass B. It sends the message to the same object self refers to, but starts the method lookup at the superclass's implementation instead of the current class's C. It always calls NSObject's implementation, regardless of the actual class hierarchy D. It is identical in every way to sending the message to self
B. It sends the message to the same object self refers to, but starts the method lookup at the superclass's implementation instead of the current class's — super is not a separate object -- it's a receiver-forwarding keyword. The message still goes to the same underlying instance as self, but the runtime begins searching for the method's implementation one level up the class hierarchy.
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
- (instancetype)initWithName:(NSString *)name age:(NSInteger)age;
@end
@implementation Person
- (instancetype)initWithName:(NSString *)name age:(NSInteger)age {
self = [super init];
if (self) {
self.name = name;
self.age = age;
}
return self;
}
@end@interface Person (Convenience)
+ (instancetype)personWithName:(NSString *)name;
@end
@implementation Person (Convenience)
+ (instancetype)personWithName:(NSString *)name {
return [[Person alloc] initWithName:name age:0];
}
@end
// Usage:
Person *sam = [Person personWithName:@"Sam"]; // age defaults to 0- (instancetype)initWithName:(NSString *)name age:(NSInteger)age {
self = [super init];
if (self) {
if (age < 0) {
return nil; // invalid input: construction fails
}
self.name = name;
self.age = age;
}
return self;
}
// Caller MUST check the result:
Person *p = [[Person alloc] initWithName:@"Sam" age:-5];
if (p != nil) {
NSLog(@"Created %@", p.name);
} else {
NSLog(@"Failed to create Person: invalid age");
}