|

Optional Chaining: Let Absence Flow Through an Expression

Branching chalk lines that stop at missing links, Molecules of Swift #005

Imagine reading a setting from an account that might not exist yet. The account may have no preferences, and the preference itself may be false. Those are three different situations. A long set of if statements can handle them, but sometimes we only need to ask for the value and let a missing link remain missing.

That is what optional chaining does. In an expression such as account?.preferences?.notificationsEnabled, each ? says: continue only if this optional contains a value. If one link is nil, the expression returns nil. If every link exists, Swift reaches the property at the end.

The interesting part is not just shorter syntax. A chain also decides which operations run, what type the result has, and whether work on the other side of an assignment happens at all.

Consider two small types:

struct Preferences { var notificationsEnabled: Bool }
struct Account { var preferences: Preferences? }

let account: Account? = Account(
    preferences: Preferences(notificationsEnabled: false)
)
let enabled: Bool? = account?.preferences?.notificationsEnabled
// enabled is .some(false), not nil

The first ? follows account, because the account is optional. The second follows preferences, because that property is optional too. notificationsEnabled is a plain Bool, so it needs no extra ?. Still, the whole read has type Bool?: any earlier link could have been missing.

This matters when the value is false. The setting is present and disabled; it is not missing. Optional chaining keeps that difference. If account or preferences were nil, the result would be nil instead.

Chaining can continue through a property that is already optional:

struct Address { var city: String? }
struct Person { var address: Address? }

let person: Person? = Person(address: Address(city: "Kyiv"))
let city: String? = person?.address?.city
let length: Int? = person?.address?.city?.count
// city == "Kyiv", length == 4

The result of person?.address?.city is String?, not String???. Each ? marks a place where the read can stop; it does not keep adding optional layers to the final type. If the city is missing, city and length are nil. If it exists, ?.count reads its length.

Notice where the last question mark sits: after city, before .count. We are chaining on the optional string returned by that property, not on count itself. The same rule applies to a method that returns an optional: the next ? goes after the method call.

Optional chaining is also allowed on the left side of an assignment. This makes its behavior more important than a simple read:

struct Note { var text: String }
var note: Note? = nil
var calls = 0
let makeText: () -> String = {
    calls += 1
    return "Ready"
}

note?.text = makeText()
// calls == 0: makeText() was not called

Because note is nil, Swift does not evaluate the right-hand side. This is not the same as building a value and then deciding not to store it. The value is never built. When note has a value, the assignment proceeds and makeText() runs.

A chained assignment has type Void?. The result is nil when the assignment could not reach its target, or a present () when it succeeded. A method returning Void behaves similarly when called through a chain: its call has type Void?. You can check that result if you need to know whether the operation ran, although an explicit if let may explain a more complex decision better.

A dictionary entry is a useful chain

The same rule works with subscripts. A dictionary lookup returns an optional because a key may not exist. We can use that optional to change an array stored under an existing key:

var grades = ["Ada": [80, 90]]
grades["Ada"]?.append(100)
grades["Leo"]?.append(100)
// grades["Ada"] == [80, 90, 100]
// grades["Leo"] == nil

The second call does not create an entry for "Leo". It skips the append because the dictionary subscript returned nil. This is handy when missing data should mean “do nothing.” If a missing key should create an empty array first, a dictionary default subscript or an explicit branch expresses a different rule.

There is a limit worth keeping in mind. Chaining checks only the optional links. It does not make an existing array index valid, stop a force unwrap from failing, or turn a throwing method into a non-throwing one. For example, grades["Ada"]?[99] can still fail at runtime because the array exists but index 99 is outside its bounds.

Poof! Let’s rewind...

In #004 — Optional Binding: Turn an Optional Into a Value, if let gave a present value a local name. Optional chaining makes a different choice: it keeps the result optional and lets the absence travel through the expression. You can still use if let on that final result when you need to work with a confirmed value.

Choose whether absence needs an answer

Chaining is useful for optional relationships, optional settings, method calls, and dictionary values when a missing link is a normal result. It keeps the read close to the data path, without creating a temporary name for every level.

But short code is not always clearer. If you need to tell the user which piece is missing, or each failure needs a different response, separate checks with guard or if let make that decision visible. And if the last value is an empty string, chaining will return that empty string—not nil. Validation is still a separate rule.

For a display label, a fallback can be enough: person?.address?.city ?? "Unknown". For a required city, binding and checking !city.isEmpty is more precise. These are different product choices, not different ways to write the same behavior.

Play with it

The Molecule #005 companion checks missing and present links, false versus nil, skipped assignments and method arguments, Void? results, dictionary updates, and explicit fallbacks. Change one link to nil, then watch which expressions still run.

Optional chaining does not make every operation safe. It makes absence at a specific link part of the expression. That is its useful promise: continue along the path when the value exists, and keep the missing result when it does not. The moment missing data needs an explanation or a recovery action, a branch is often the clearer tool.

Sources