Optional chaining lets you call properties, methods, and subscripts on a value that might be nil, in one expression. If any link in the chain is nil, the whole chain quietly evaluates to nil instead of crashing. It’s the polite alternative to force-unwrapping with !, and you’ll use it every time you write delegate?.didFinish() or self?.update().
Optional Chaining in Swift
A quick reminder: optionals are enums
An optional is either .none (written nil) or .some(value):
enum MyOptional<Wrapped> {
case none
case some(Wrapped)
}
Everything about optional chaining follows from that: to reach a value inside an optional, you first have to check which case it is. Optional chaining is a compact way to say “if it’s .some, continue; if it’s .none, stop and produce nil.”
The problem it solves
final class Residence {
var numberOfRooms = 1
var address: Address?
var rooms: [String] = []
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
}
struct Address {
var street: String?
var city = "Unknown"
func formatted() -> String {
"\(street ?? "No street"), \(city)"
}
}
final class Resident {
var residence: Residence?
}
let john = Resident()
john.residence is nil. Force-unwrapping it crashes your app:
// let rooms = john.residence!.numberOfRooms // crash: unexpectedly found nil
Chaining with ?
Put ? after the optional instead, and the rest of the expression only runs if there’s a value:
if let rooms = john.residence?.numberOfRooms {
print("John's residence has \(rooms) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
Notice the type. numberOfRooms is an Int, but john.residence?.numberOfRooms is an Int?. The result of optional chaining is always optional, because the chain might fail. That’s the whole trick: the possibility of nil is carried in the type, so you have to deal with it before using the value.
Chaining through several levels
Chains can go as deep as you need, and they stop at the first nil:
john.residence = Residence()
let street = john.residence?.address?.street // String?: nil, because address is nil
john.residence?.address = Address(street: "1 Infinite Loop", city: "Cupertino")
print(john.residence?.address?.street ?? "none") // 1 Infinite Loop
Optionals don’t stack up
street is itself a String?. Reaching it through two optional links still gives you a String?, not a String??. Optional chaining flattens the result to a single level of optionality:
let words: [String]? = ["a", "bb"]
let firstWordLength = words?.first?.count // Int?, not Int??
print(firstWordLength ?? 0) // 1
The same flattening applies to try? since Swift 5: calling a throwing function that returns Int? with try? gives you an Int?, not an Int??.
Calling methods
let description = john.residence?.address?.formatted() // String?
Even methods that return nothing can be chained. A Void method called through a chain returns Void?, which tells you whether the call actually happened:
let nobody = Resident()
if nobody.residence?.printNumberOfRooms() != nil {
print("It was possible to print the number of rooms.")
} else {
print("It was not possible to print the number of rooms.")
}
Setting values through a chain
Assignment through an optional chain does nothing if any link is nil, and the right-hand side isn’t even evaluated:
func makeAddress() -> Address {
print("Building address")
return Address(street: "Main St")
}
nobody.residence?.address = makeAddress() // "Building address" is never printed
That’s both efficient and occasionally surprising: if makeAddress() had a side effect you relied on, it wouldn’t happen. The assignment also returns Void?, so you can check whether it succeeded with != nil.
Subscripts
Put the ? before the brackets when the value being subscripted is optional:
john.residence?.rooms.append("Living Room")
print(john.residence?.rooms[0] ?? "no room") // Living Room
var testScores: [String: [Int]]? = ["Dave": [86, 82, 84]]
let first = testScores?["Dave"]?[0] // 86, as Int?
testScores?["Dave"]?[0] = 91
testScores?["Brian"]?[0] = 72 // no "Brian": silently does nothing
print(testScores ?? [:])
In testScores?["Dave"]?[0], the first ? handles the dictionary itself being nil, and the second handles the key "Dave" being missing, because a dictionary subscript returns an optional.
Note that rooms[0] inside a chain still traps if rooms is empty. Optional chaining only protects you from nil, not from invalid indices.
Where you’ll use it every day
Delegates and callbacks, which are optional by nature:
protocol DownloaderDelegate: AnyObject {
func downloaderDidFinish()
}
final class Downloader {
weak var delegate: DownloaderDelegate?
var onProgress: ((Double) -> Void)?
func finish() {
onProgress?(1.0) // call an optional closure
delegate?.downloaderDidFinish()
}
}
Weak self in closures, where self?.method() simply does nothing if the object is gone:
final class Screen {
var title = ""
func load(using fetch: (@escaping (String) -> Void) -> Void) {
fetch { [weak self] result in
self?.title = result // no-op if the screen was closed
}
}
}
Defaults with ??: optional chaining and the nil-coalescing operator are a natural pair:
let city = john.residence?.address?.city ?? "Unknown city"
let text: String? = nil
let isEmpty = text?.isEmpty ?? true // treat nil as empty
let length = text?.count ?? 0
The right side of ?? is only evaluated when needed, so value ?? expensiveDefault() doesn’t call the function unless value is nil.
Optional chaining vs. map and flatMap
Optionals have map and flatMap methods that do the same job in functional style. They’re useful when you want to transform an optional with a function that isn’t a member of the wrapped type:
let maybeNumber: String? = "42"
let parsed = maybeNumber.flatMap { Int($0) } // Int?: flatMap avoids Int??
let doubled = parsed.map { $0 * 2 } // Int?
print(doubled ?? 0) // 84
a?.b is equivalent to a.map { $0.b } when b isn’t optional, and to a.flatMap { $0.b } when it is. Use ?. for members, and map/flatMap for free functions and initializers like Int(_:).
Under the hood
Optional chaining compiles to a simple check of the optional’s case at each link, followed by an early exit to “produce nil”. There’s no exception machinery and no hidden allocation. For optionals of class references, nil is represented as a null pointer, so the check is a single comparison against zero. Chaining is as cheap as the if let code you’d write by hand.
When not to chain
- When you need to know which link failed.
a?.b?.c?.dgives younilwithout saying why. If that matters, for an error message or for logging, unwrap step by step withguard letand handle each case. - When a value should never be
nil. Ifresidencemust always exist, make it non-optional, rather than chaining around it everywhere and hiding a real bug. - When the silent no-op is dangerous.
account?.withdraw(100)silently doing nothing might be worse than an error.
Common pitfalls
- Expecting optional chaining to prevent crashes from invalid indices. It only handles
nil. - Comparing chained booleans.
user?.isAdmin == truereads oddly but is correct:user?.isAdminis aBool?, and comparing withtruehandlesnilas “not admin”.if user?.isAdmin { … }doesn’t compile, because aBool?isn’t aBool. - Forgetting that assignments may be skipped, along with any side effects on the right-hand side.
Interview questions
What’s the type of person.residence?.numberOfRooms if numberOfRooms is an Int?
Int?. Optional chaining always produces an optional.
Does chaining through two optionals give Int???
No. Optional chaining flattens to a single level of optionality.
What happens to the right-hand side of a?.b = makeValue() when a is nil?
It isn’t evaluated at all.
How do you call an optional closure?
completion?(result). It calls the closure if it’s non-nil, and does nothing otherwise.
Summary
?.accesses a property, method, or subscript only when the optional has a value; otherwise the whole chain isnil.- The result of a chain is always optional, and nested optionals are flattened.
- Assignments through a
nilchain are skipped, including evaluating the right-hand side;Voidcalls returnVoid?. - Pair chains with
??for defaults; usemap/flatMapfor free functions; unwrap step by step when you need to know what failed.