Initializer Design

Master the deeper rules of Objective-C initializer design: designated vs. convenience initializer chaining, NS_DESIGNATED_INITIALIZER enforcement, correct subclass override patterns, initialization failure, partially initialized objects, and the requirements of immutable object initialization.

The earlier Object Creation: alloc and init and Initialization Patterns modules covered the basics of writing an -init method — allocate with +alloc, initialize with -init, and always assign self = [super init] before doing anything else. This addendum lesson goes considerably deeper, into the edge cases and design rules that separate a merely-working initializer from a correctly-designed one: the formal distinction between designated and convenience initializers and the strict chaining rules connecting them, what NS_DESIGNATED_INITIALIZER (whose basic syntax the sibling Objective-C API Annotations lesson introduces) actually enforces at compile time, the precise subclass override pattern required to extend a superclass's initialization safely, how and why an initializer signals failure by returning nil, the specific danger window during which an object is only partially initialized, and the special discipline immutable objects require since they have no setters to fall back on after -init returns.

Designated vs. Convenience Initializers

Every Objective-C class with more than one initializer should have exactly one designated initializer — the single initializer that actually performs the full, real work of setting up every stored property the class owns, either directly or by delegating to its superclass's designated initializer. Every OTHER initializer the class offers is a convenience initializer: a simpler or more specialized entry point that ultimately funnels through the designated initializer rather than duplicating its setup logic.

The chaining rule that ties these together is precise and worth stating exactly: a convenience initializer must call another initializer on self (either the designated initializer directly, or another convenience initializer that itself eventually reaches the designated initializer), while the designated initializer must call an initializer on super — specifically, the superclass's own designated initializer. This creates a clean, single-threaded chain of initialization: convenience initializers funnel sideways into the designated initializer within the same class, and the designated initializer funnels upward into the superclass's designated initializer, all the way up to NSObject's -init at the root.

The reason this discipline matters is that it guarantees full object setup happens in exactly one place, no matter which initializer a caller happens to use. If each convenience initializer duplicated the full setup logic independently instead of delegating to the designated initializer, every future change to the class's setup logic would need to be made in every convenience initializer simultaneously — an easy way to introduce subtle inconsistencies where objects created through different initializers end up in different states.

NS_DESIGNATED_INITIALIZER

NS_DESIGNATED_INITIALIZER is a macro you attach directly to an initializer's declaration in the @interface to explicitly mark it as the class's designated initializer, rather than leaving that fact as an unenforced convention documented only in a comment. Once at least one initializer in a class is marked this way, the compiler switches into a stricter checking mode for that class's entire set of initializers, and begins enforcing the chaining rule described above automatically rather than just hoping developers follow it.

Concretely, once NS_DESIGNATED_INITIALIZER is used, the compiler requires that every OTHER initializer in that same class call self, not super — meaning it must be written as a convenience initializer chaining sideways to the designated one — and it requires that the designated initializer itself call the superclass's designated initializer via super, not just any arbitrary superclass initializer. Violating either half of this produces a compiler warning: calling super from what should be a convenience initializer, or calling self from the designated initializer, or a designated initializer calling a superclass initializer that isn't itself marked as that superclass's designated initializer, all trigger a diagnostic warning at compile time — catching a class of initializer-chaining bugs that, before this macro existed, could only be caught by careful code review or by the bug actually manifesting at runtime.

A second, equally important effect: if a subclass does not override ANY of its superclass's designated initializers, and that superclass has NS_DESIGNATED_INITIALIZER-marked initializers, the subclass automatically inherits the superclass's designated initializer(s) as its own — but the moment a subclass overrides even one, all of the superclass's other convenience initializers become unavailable on the subclass unless the subclass also implements its own designated initializer that properly chains through them. This mechanism is precisely what stops a subclass from silently ending up with a convenience initializer that skips whatever extra setup the subclass's own designated initializer was supposed to guarantee.

Subclass Initialization and [super init]

When a subclass overrides its superclass's designated initializer, the correct override pattern always follows the same three-step shape: call the superclass's designated initializer first via super, assign the result back to self, check that the result isn't nil, and only then proceed to set up whatever additional state the subclass itself adds — all before returning self at the end.

Calling [super init...] FIRST, before touching any of the subclass's own instance variables or properties, matters for a concrete reason: until the superclass's portion of initialization has completed successfully, the object's superclass-owned instance variables are not guaranteed to be in a valid, fully-set-up state, and any subclass-level setup code that assumes otherwise (for instance, code that calls an overridden method the superclass also relies on internally during its own init) risks operating on a half-initialized object. Calling super's initializer first, and only proceeding once that call has returned successfully, guarantees the object is in a valid base state before the subclass layers its own state on top.

The three-step shape written out concretely looks like: self = [super initWithName:name]; then if (self) { self->_extraProperty = value; } then return self; — every single Objective-C initializer, across every class, follows this exact same shape, with the specific superclass initializer called and the specific extra setup performed being the only things that vary from class to class.

Initialization Failure and the if (self) Pattern

An Objective-C initializer signals that initialization failed — that the object could not be brought into a valid, usable state — by returning nil instead of self. This might happen because a required resource couldn't be obtained (a file couldn't be opened, a network connection couldn't be established), because an argument failed validation (an empty required name string, an out-of-range value), or because the superclass's own initializer itself returned nil, in which case the subclass's initializer should propagate that failure by also returning nil rather than attempting to continue setting up a subclass on top of a superclass that failed to initialize.

This is precisely why every initializer follows the self = [super init]; if (self) { ... } return self; pattern rather than simply writing [super init]; ... return self; without the intermediate check. Assigning the result of [super init] back to self, rather than discarding it, is what lets the subclass see whether the superclass's initializer succeeded (returned a valid pointer) or failed (returned nil) — and the if (self) check is what prevents the subclass's own setup code from ever running against a nil, failed superclass result.

It's worth noting explicitly why self itself gets reassigned rather than just checked: because although rare, some initializers legitimately need to return a DIFFERENT object entirely than the one -alloc originally created — a class cluster's initializer, for instance, may free the originally-allocated instance and return a differently-configured instance in its place. Assigning the superclass's return value back into self is what correctly accounts for this possibility as well as the simple failure case, which is why the pattern is self = [super init], not merely if ([super init] != nil).

Partially Initialized Objects

A partially initialized object is an object that -alloc has allocated memory for, but whose initialization is not yet complete — either because [super init...] hasn't been called yet, or because it has been called but has not yet returned, or because it returned nil (signaling failure) but that nil wasn't checked before further code ran anyway. Using self, or any instance variable self owns, during this window — before the if (self) check has confirmed successful superclass initialization — is dangerous precisely because the object may not actually be in the state the rest of your code assumes.

The concrete danger scenario looks like this: if a subclass's init method calls an overridden method of its own (rather than accessing an ivar directly) BEFORE self = [super init] has completed, and that overridden method happens to depend on state the superclass's initializer was supposed to set up, the method runs against instance variables that are still at their raw, zero-filled, freshly-allocated state — not the values the superclass's initializer was going to assign them, because that hasn't happened yet. This is one of the most common sources of the 'why is this property nil/zero when I know I set it' class of Objective-C bugs, and it is precisely why disciplined initializer authors avoid calling overridable instance methods from within an initializer until after [super init...] has both been called and been confirmed non-nil.

The if (self) check is the single line of code standing between a correctly initialized object and a partially initialized one silently being used as if it were complete — treating the check as optional boilerplate, rather than as the guard it actually is, is exactly how partially-initialized-object bugs make it into shipped code.

Immutable Object Initialization

Immutable objects — objects whose properties are meant to be fixed for the object's entire lifetime once created, with no setter methods offered at all — place a stricter requirement on their initializer than ordinary mutable objects do: every single property the object needs must be fully set during -init, because there is no later opportunity to set it. A mutable object that forgets to initialize some property can often get away with it temporarily, since a caller might set that property afterward through a setter before actually using the object; an immutable object has no such safety net, since by design it offers no setter for a caller to fall back on.

This makes required initialization arguments the norm for immutable classes rather than the exception: an immutable class's designated initializer typically takes every value the object needs as a parameter directly (- (instancetype)initWithName:(NSString *)name age:(NSInteger)age NS_DESIGNATED_INITIALIZER;), and often the class deliberately makes its bare -init unavailable (commonly by overriding it to call NSAssert(NO, ...) or marking it NS_UNAVAILABLE) specifically to prevent a caller from constructing a half-formed instance with no name and no age and no way to ever supply them afterward.

Designed this way, an immutable object's validity is guaranteed once and permanently at construction time: if -initWithName:age: returns a non-nil self, every caller of that object, for its entire lifetime, can rely on name and age being present and correct, with no need to defensively check for a missing or later-set value anywhere else in the codebase that consumes that object. This guarantee is exactly why immutable object design and rigorous designated-initializer discipline go hand in hand — one enables the other.

Designated Initializer Declaration and Override Pattern

A convenience initializer calling super instead of self triggers a compiler warning once NS_DESIGNATED_INITIALIZER is used anywhere in the class. The designated initializer must call the superclass's OWN designated initializer, not just any superclass initializer. self = [super init]; if (self) { ... } return self; is the universal three-step shape for every initializer.

// Designated initializer, explicitly marked
@interface Person : NSObject

- (instancetype)initWithName:(NSString *)name
    NS_DESIGNATED_INITIALIZER;

// Convenience initializer — must call self, not super
- (instancetype)init;

@end

@implementation Person

- (instancetype)initWithName:(NSString *)name {
    self = [super init];           // designated init calls super's designated init
    if (self) {
        _name = [name copy];
    }
    return self;
}

- (instancetype)init {
    return [self initWithName:@"Unnamed"]; // convenience chains sideways to self
}

@end

// Subclass override pattern
@implementation Employee

- (instancetype)initWithName:(NSString *)name
                    salary:(double)salary {
    self = [super initWithName:name]; // call super's designated init FIRST
    if (self) {
        _salary = salary;             // then set up subclass-specific state
    }
    return self;
}

@end

Code Examples

Marking this initializer with NS_DESIGNATED_INITIALIZER tells the compiler this is the one true full-setup initializer for the class, switching on strict chaining enforcement for every other initializer declared in the same @interface.

A convenience initializer calls another initializer on self, never on super directly — here it funnels into the class's own designated initializer, initWithName:, ensuring the full setup logic runs exactly once regardless of which initializer a caller used.

Returning nil directly, before even calling [super init], is a valid and common failure path when argument validation fails early. Callers of any initializer must always be prepared for a nil result and check it before using the object.

configureFromDefaults is only safe to call once self has been confirmed non-nil after [super init]. Calling it earlier — before the if (self) check — would run it against a partially initialized object whose superclass-owned state hasn't been set up yet.

Common mistake: Calling an overridable helper method from inside an initializer before [super init] has both run and been checked for nil risks operating on instance variables that are still at their raw, zero-filled state.

- (instancetype)initWithName:(NSString *)name
    NS_DESIGNATED_INITIALIZER;
- (instancetype)init {
    return [self initWithName:@"Unnamed"];
}
- (instancetype)initWithName:(NSString *)name {
    if (name.length == 0) {
        return nil; // fail: an empty name is not a valid Person
    }
    self = [super init];
    if (self) {
        _name = [name copy];
    }
    return self;
}

Person *p = [[Person alloc] initWithName:@""];
// p is nil here — the caller must check before using it
@implementation Logger

- (instancetype)init {
    self = [super init];
    if (self) {
        [self configureFromDefaults]; // safe: runs AFTER self is confirmed valid
    }
    return self;
}

- (void)configureFromDefaults {
    // Dangerous if called BEFORE self = [super init] completes:
    // _prefix, set up by a superclass initializer, would still be nil here.
    _prefix = [_prefix stringByAppendingString:@"[LOG] "];
}

@end

Where This Shows Up

Foundation's own immutable value classes — NSString, NSNumber, NSURL — are the textbook illustration of immutable object initialization discipline: every one of them requires its full value at construction time through a designated initializer or factory method, offers no setters whatsoever, and is therefore guaranteed valid and unchanging for its entire lifetime once created. On the failure side, initializers like NSData's -initWithContentsOfFile: legitimately return nil when the file can't be read, and any code that skips checking that nil before using the resulting object is a live crash waiting for the day that file happens to be missing.

• (instancetype)initWithName:(NSString *)name { [super init]; // return value discarded — self never reassigned _name = [name copy]; return self; } - (instancetype)initWithName:(NSString *)name { self = [super init]; if (self) { _name = [name copy]; } return self; }

Discarding the return value of [super init] instead of assigning it back to self means the initializer has no way to detect a failed (nil) superclass initialization, and continues setting up instance variables on whatever self already pointed to — which is dangerous if the superclass legitimately returned nil or a different object entirely.

What must a convenience initializer call, and what must the designated initializer call, under correct chaining rules? A. Both must call super B. The convenience initializer calls super; the designated initializer calls self C. The convenience initializer calls another initializer on self; the designated initializer calls the superclass's designated initializer via super D. Neither is required to call any other initializer

C. The convenience initializer calls another initializer on self; the designated initializer calls the superclass's designated initializer via super — Convenience initializers chain sideways within the same class by calling self; the designated initializer chains upward by calling super's own designated initializer, forming a single, predictable initialization path.

What does adding NS_DESIGNATED_INITIALIZER to one initializer in a class cause the compiler to do? A. Nothing observable — it is purely documentation B. It enforces strict chaining rules for every initializer in the class, warning when a convenience initializer calls super or the designated initializer calls self C. It automatically generates a convenience initializer for every property D. It prevents the class from ever being subclassed

B. It enforces strict chaining rules for every initializer in the class, warning when a convenience initializer calls super or the designated initializer calls self — Once used, NS_DESIGNATED_INITIALIZER switches the compiler into strict checking mode for the whole class, producing warnings when the chaining rules (convenience → self, designated → super's designated) are violated.

In a correct subclass initializer override, what must happen before the subclass sets up its own additional instance variables? A. Nothing — order does not matter as long as return self is last B. The superclass's designated initializer must be called via super, assigned back to self, and confirmed non-nil first C. The subclass must call [self init] before calling super D. The subclass must allocate a second object and copy values into it

B. The superclass's designated initializer must be called via super, assigned back to self, and confirmed non-nil first — The correct pattern always calls super's designated initializer first, reassigns the result to self, and checks it for nil before the subclass proceeds to set up any of its own additional state.

How does an Objective-C initializer signal that initialization has failed? A. By throwing an NSException B. By returning nil instead of self C. By setting a BOOL out-parameter to NO D. By calling abort()

B. By returning nil instead of self — Returning nil in place of self is the standard way an Objective-C initializer signals failure. Callers must always check the result of any initializer before using it.

Why is it dangerous to call an overridable instance method from within an initializer before self = [super init]; if (self) has completed? A. It isn't dangerous — Objective-C guarantees ivars are always valid immediately after alloc B. It may run against instance variables that are still in their raw, zero-filled, not-yet-initialized state C. It causes a compile-time error D. It always crashes immediately with an unrecognized selector error

B. It may run against instance variables that are still in their raw, zero-filled, not-yet-initialized state — Before the superclass's initializer has completed and been confirmed non-nil, the object is only partially initialized — instance variables the superclass was supposed to set up have not been set yet, so any method relying on that state operates on invalid data.

A Vehicle class declares - (instancetype)initWithWheelCount:(NSInteger)count NS_DESIGNATED_INITIALIZER; and a convenience - (instancetype)init;. A subclass, Car, adds its own initializer - (instancetype)initWithColor:(NSString *)color but implements it by calling [super init] instead of [super initWithWheelCount:4]. Explain what the compiler will do, and correct the implementation.

Because Vehicle marks initWithWheelCount: as its designated initializer, Car's own designated-style initializer must chain to that specific superclass designated initializer, not to the generic convenience -init. Calling [super init] here triggers a compiler warning about incorrect initializer chaining. The corrected implementation is: self = [super initWithWheelCount:4]; if (self) { _color = [color copy]; } return self;

Recall exactly which superclass initializer a subclass's designated initializer must call once NS_DESIGNATED_INITIALIZER is in use.