Swift Initializers: Designated, Convenience, and Failable Initializers

Swift initializers are the methods that create a valid instance of a class or structure by assigning its stored properties before use. This article explains the three initializer styles you will see most often in Swift classes: designated initializers, convenience initializers, and failable initializers.

Quick answer: A designated initializer does the main setup work, a convenience initializer is a helper that must call another initializer in the same type, and a failable initializer can return nil when initialization cannot succeed.

Difficulty: Beginner to Intermediate

Helpful to know first: You'll understand this better if you know how Swift stores values in properties, what classes are, and how let and var work.

1. What Is Swift Initializers?

An initializer is the code that runs when you create a new instance with init. Its job is to ensure the instance starts in a valid state by setting every stored property that needs a value.

2. Why Swift Initializers Matter

Swift uses initializers to prevent partially created objects from escaping into your program. That makes your code safer and easier to reason about, especially when values depend on each other or when invalid input should stop construction early.

In practice, initializers matter because they help you:

3. Basic Syntax or Core Idea

A basic initializer uses init followed by parameters and a body that assigns values to stored properties.

Simple class initializer

Here is the minimal pattern for a class with a single initializer.

class User {
    var name: String
    var age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

This initializer takes two parameters, then assigns them to the instance properties using self.

Creating an instance

Once the initializer exists, you create a value by calling the type like a function.

let user = User(name: "Ava", age: 28)

The value is fully initialized only after the initializer finishes successfully.

4. Step-by-Step Examples

Example 1: Designated initializer for a class

A designated initializer is the primary initializer for a class. It is responsible for setting all stored properties introduced by that class.

class Book {
    var title: String
    var author: String

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

let book = Book(title: "1984", author: "George Orwell")

This is the main place to put required setup logic for a class.

Example 2: Convenience initializer that supplies a default

A convenience initializer is a helper initializer. It must call another initializer in the same type using self.init.

class Temperature {
    var celsius: Double

    init(celsius: Double) {
        self.celsius = celsius
    }

    convenience init() {
        self.init(celsius: 0)
    }
}

let freezingPoint = Temperature()

The convenience initializer reduces repeated code when a default value makes sense.

Example 3: Failable initializer for validation

A failable initializer returns an optional instance. Use it when the input might be invalid and the type cannot be created safely.

struct Password {
    let value: String

    init?(value: String) {
        guard value.count >= 8 else {
            return nil
        }

        self.value = value
    }
}

let strongPassword = Password(value: "secure123")

If the string is too short, initialization fails and the result is nil.

Example 4: Required initializer in a subclass

When a superclass marks an initializer as required, every subclass must implement it or inherit it if possible.

class Vehicle {
    var wheels: Int

    required init(wheels: Int) {
        self.wheels = wheels
    }
}

class Bike: Vehicle {
    required init(wheels: Int) {
        super.init(wheels: wheels)
    }
}

This rule helps Swift preserve initializer availability across inheritance hierarchies.

5. Practical Use Cases

Swift initializers are useful any time an object needs setup logic that should happen exactly once at creation time.

6. Common Mistakes

Mistake 1: Forgetting to initialize every stored property

Swift requires all non-optional stored properties to have values before the initializer ends. This is one of the most common compile-time errors for beginners.

Problem: The class leaves age without a value, so Swift reports that the instance is not fully initialized.

class Person {
    var name: String
    var age: Int

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

Fix: Assign a value to every required stored property, or make the property optional if absence is valid.

class Person {
    var name: String
    var age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

The corrected version works because Swift can prove the instance is complete before it leaves the initializer.

Mistake 2: Calling the wrong initializer from a convenience initializer

A convenience initializer must delegate to another initializer in the same type. It cannot stop after setting only some properties.

Problem: This code tries to assign the property directly inside a convenience initializer instead of delegating properly.

class Score {
    var value: Int

    init(value: Int) {
        self.value = value
    }

    convenience init() {
        self.value = 0
    }
}

Fix: Delegate from the convenience initializer to a designated initializer using self.init.

class Score {
    var value: Int

    init(value: Int) {
        self.value = value
    }

    convenience init() {
        self.init(value: 0)
    }
}

The fixed version works because the designated initializer remains the single place that fully initializes the object.

Mistake 3: Using a failable initializer as if it always succeeds

A failable initializer returns an optional, so you must handle the possibility that the result is nil.

Problem: This code assumes the initializer always returns a value, but a failable initializer produces an optional instead.

struct Username {
    let value: String

    init?(value: String) {
        guard !value.isEmpty else {
            return nil
        }

        self.value = value
    }
}

let username: String = Username(value: "swiftuser")

Fix: Bind the optional result with if let, guard let, or optional chaining.

struct Username {
    let value: String

    init?(value: String) {
        guard !value.isEmpty else {
            return nil
        }

        self.value = value
    }
}

if let username = Username(value: "swiftuser") {
    print(username.value)
}

The corrected version works because it treats failure as a normal possible outcome.

7. Best Practices

Practice 1: Put required setup in the designated initializer

Designated initializers should own the core setup logic so the class has one clear source of truth.

class Account {
    let id: String
    var balance: Double

    init(id: String, balance: Double) {
        self.id = id
        self.balance = balance
    }
}

This approach makes it easier to keep invariants in one place.

Practice 2: Use convenience initializers for common defaults only

Convenience initializers are best when they remove repetition, not when they replace the real initialization rules.

class TimerConfig {
    var duration: Int

    init(duration: Int) {
        self.duration = duration
    }

    convenience init(defaultDuration: Bool) {
        self.init(duration: defaultDuration ? 60 : 30)
    }
}

This keeps the defaulting behavior separate from the real property setup.

Practice 3: Prefer failable initializers for invalid data, not runtime crashes

If input can be rejected, a failable initializer is often cleaner than forcing a crash later.

struct Port {
    let number: Int

    init?(number: Int) {
        guard (1...65535).contains(number) else {
            return nil
        }

        self.number = number
    }
}

This gives the caller a clear way to handle invalid values without crashing the program.

8. Limitations and Edge Cases

9. Practical Mini Project

Let’s build a small Recipe type that uses all three initializer styles in a realistic way.

The type has one designated initializer for the core data, one convenience initializer for a common default, and one failable initializer that rejects an invalid recipe name.

class Recipe {
    let name: String
    var servings: Int

    init(name: String, servings: Int) {
        self.name = name
        self.servings = servings
    }

    convenience init(name: String) {
        self.init(name: name, servings: 4)
    }

    convenience init?(rawName: String) {
        let trimmed = rawName.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else {
            return nil
        }

        self.init(name: trimmed, servings: 4)
    }
}

let defaultRecipe = Recipe(name: "Soup")
let validatedRecipe = Recipe(rawName: "  Pasta  ")

This project shows how the three initializer types work together: one initializer owns the core rules, one supplies a default, and one rejects invalid input safely.

10. Key Points

11. Practice Exercise

Expected output: A valid task should print its title and priority, while the invalid task should evaluate to nil.

Hint: Use guard inside the failable initializer to reject empty titles before assigning properties.

class Task {
    let title: String
    var priority: Int

    init(title: String, priority: Int) {
        self.title = title
        self.priority = priority
    }

    convenience init() {
        self.init(title: "Untitled", priority: 1)
    }

    convenience init?(title: String) {
        guard !title.isEmpty else {
            return nil
        }

        self.init(title: title, priority: 1)
    }
}

let task1 = Task(title: "Write docs", priority: 3)
let task2 = Task()
let task3 = Task(title: "")

print(task1.title, task1.priority)
print(task2.title, task2.priority)
print(task3 == nil ? "nil" : "not nil")

12. Final Summary

Swift initializers are the foundation of safe object creation. They make sure a value is valid before anyone uses it, which is why Swift is strict about setting every required property and following the initialization rules for classes.

Designated initializers do the core work, convenience initializers reduce repetition, and failable initializers let you represent invalid input without crashing. Once you understand how these three initializer styles cooperate, you can design class APIs that are clear, safe, and easy to use.

If you want to go further, the next useful topic is how initializer inheritance works in subclasses and how Swift decides when inherited initializers are available automatically.