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.
- init is the initializer keyword in Swift.
- Initializers can take parameters, just like functions.
- Classes have initializer rules about inheritance and delegation.
- Structs and enums can also have initializers, but the rules are simpler.
- Failable initializers use init? and may return nil.
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:
- Guarantee that every instance starts with valid property values.
- Encapsulate setup logic instead of repeating it in multiple places.
- Offer different creation paths without exposing implementation details.
- Reject invalid data cleanly with failable initializers.
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.
- Creating model objects such as users, products, orders, and settings.
- Validating text input before storing it in a type.
- Providing convenience defaults like empty states, preview data, or common configurations.
- Building class hierarchies where subclasses need to extend setup from a superclass.
- Wrapping external data sources where invalid data should fail early.
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
- Class initialization has stricter rules than struct initialization because classes support inheritance.
- Subclass initializers must ensure superclass properties are initialized correctly before the instance is fully usable.
- Property default values can reduce initializer code, but they do not remove the need for initialization rules.
- A failable initializer cannot both fail and return a partially initialized instance.
- Optional properties may be left unset initially, but that does not mean the initializer can ignore the rest of the stored properties.
- Some inherited initializers are not available automatically if the subclass adds new required properties or custom initialization rules.
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
- Initializers create a valid instance and assign all required stored properties.
- Designated initializers do the main setup work for a class.
- Convenience initializers must delegate to another initializer in the same type.
- Failable initializers use init? and return nil on failure.
- Swift enforces initialization rules at compile time to prevent invalid objects.
- Inheritance makes class initializers more structured than struct initializers.
11. Practice Exercise
- Create a Task class with a designated initializer that accepts a title and priority.
- Add a convenience initializer that creates a default low-priority task.
- Add a failable initializer that rejects empty titles.
- Test each initializer by creating at least one valid task and one invalid task.
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.