Swifty Journey

Swift from Zero to Expert #11: Optionals & Optional Chaining

Swift's antidote to the billion-dollar mistake. Optional is just an enum — and nil, if let, guard let, ??, !, and optional chaining all compile down to checking which case you're in.

In the previous article we walked a class from birth to death — init guaranteeing a valid object, deinit handing back what it borrowed. We even saw a failable initializer (init?) hand you back an optional instance, and promised the full story here. This is that story.

Optionals are Swift’s answer to the billion-dollar mistake — Tony Hoare’s name for the null reference he invented in 1965, which has caused untold crashes ever since. Swift’s fix isn’t a clever runtime trick. It’s a type. A value of type Int is always an integer. A value of type Int? is either an integer or nothing — and the type system won’t let you forget the second possibility.

An optional isn’t a special language feature bolted onto types. It’s just an enum with two cases — one that holds a value, one that doesn’t. Everything else is syntax over that single idea.

nil: the absence of a value

You’ve already met optionals without naming them. A failable initializer returns one:

let possibleNumber = "123"
let convertedNumber = Int(possibleNumber)
// convertedNumber is of type "optional Int", or "Int?"

Int(_:) can’t promise success — "123" converts cleanly, but "hello" doesn’t — so it returns an Int? rather than an Int. To represent “no value,” set an optional to nil:

var serverResponseCode: Int? = 404
serverResponseCode = nil
// serverResponseCode now contains no value

If you declare an optional variable without giving it a value, it’s automatically set to nil:

var surveyAnswer: String?
// surveyAnswer is automatically set to nil

Under the hood: Optional is an enum

Here’s the part the series exists for. Optional is not magic syntax — it’s an ordinary generic enum in the standard library. <Wrapped> is a placeholder type that gets filled in with whatever you wrap — Int? is Optional<Int>, so Wrapped becomes Int. (Generics get their own article later; here you only need that Optional<Int> means “an Optional holding an Int”.) Its declaration is essentially:

enum Optional<Wrapped> {
case none // the absence of a value — this is what `nil` means
case some(Wrapped) // the presence of a value, stored as Wrapped
}

The ? you write is pure sugar: Int? is exactly Optional<Int>. nil is exactly Optional.none. Assigning a real value wraps it in .some. The two forms are interchangeable:

let shortForm: Int? = Int("42")
let longForm: Optional<Int> = Int("42")
let number: Int? = Optional.some(42)
let noNumber: Int? = Optional.none
print(noNumber == nil) // true

Once you see this, every operator in this article stops being mysterious. Unwrapping an optional is literally the enum pattern matching from #6 — asking “am I in the .some case, and if so, what’s the associated value?” if let, guard let, ??, and ! are all just ergonomic ways to ask that one question.

Optional<Int> drawn as a single enum box with two cases — case some(42) holding a wrapped Int value, and case none which equals nil. The Int? sugar and the Optional<Int> long form are shown as two labels pointing at the same box.

Forced unwrapping: the ! that can crash

If you’re certain an optional holds a value, you can reach in and grab it with a trailing exclamation mark — forced unwrapping:

let possibleNumber = "123"
let convertedNumber = Int(possibleNumber)
if convertedNumber != nil {
print("convertedNumber has an integer value of \(convertedNumber!).")
}
// convertedNumber has an integer value of 123.

convertedNumber! says “I know this isn’t nil — give me the Int inside.” In enum terms, it asserts you’re in the .some case and returns the associated value.

Optional binding: if let and guard let

The if let form unwraps into a new constant that’s only in scope inside the if body:

let possibleNumber = "123"
if let actualNumber = Int(possibleNumber) {
print("The string \"\(possibleNumber)\" has an integer value of \(actualNumber)")
} else {
print("The string \"\(possibleNumber)\" couldn't be converted to an integer")
}
// The string "123" has an integer value of 123

If Int(possibleNumber) is .some(value), actualNumber is bound to that value and the if branch runs. If it’s .none, the else branch runs. The unwrapped actualNumber is a plain Int — no ?, no !.

When the unwrapped constant would share the optional’s name, you can use the shorthand: just write if let name with no =:

if let convertedNumber {
// convertedNumber is now a non-optional Int, named the same as the optional
print(convertedNumber)
}

You can bind several optionals (and add Boolean clauses) in one if, separated by commas. The whole thing succeeds only if every binding succeeds:

if let firstNumber = Int("4"), let secondNumber = Int("42"), firstNumber < secondNumber && secondNumber < 100 {
print("\(firstNumber) < \(secondNumber) < 100")
}
// 4 < 42 < 100

guard let: bind for the rest of the scope

if let binds inside its own little block. Often you want the opposite — unwrap once at the top of a function and use the value through the rest of the body. That’s guard let:

func greet(_ name: String?) {
guard let name else {
print("No name provided")
return
}
// name is a non-optional String from here to the end of the function
print("Hello, \(name)!")
}

A guard requires its condition to be true to continue. If the binding fails, the else block runs and must exit the current scope (return, break, throw, etc.). The payoff: the unwrapped name stays in scope for everything after the guard, with no extra indentation. This is the early-exit pattern from #5, applied to optionals.

The ?? nil-coalescing operator

let defaultColorName = "red"
var userDefinedColorName: String? // defaults to nil
var colorNameToUse = userDefinedColorName ?? defaultColorName
// userDefinedColorName is nil, so colorNameToUse is set to "red"

The right-hand side b must match the type of the unwrapped a. Here userDefinedColorName is String? and defaultColorName is String, so the result is a non-optional String.

Because ?? can take another optional on its right, you can chain it to express “try this, then that, then fall back”:

let imagePaths = ["star": "/glyphs/star.png"]
let defaultImagePath = "/images/default.png"
let shapePath = imagePaths["cir"] ?? imagePaths["squ"] ?? defaultImagePath
print(shapePath)
// /images/default.png — both lookups failed, so the final default wins

Implicitly unwrapped optionals

The motivating case: a value that’s nil for a brief window during setup, then permanently non-nil afterward. Writing ? and unwrapping it on every single access would be pure noise.

let possibleString: String? = "An optional string."
let forcedString: String = possibleString! // requires an explicit !
let assumedString: String! = "An implicitly unwrapped optional string."
let implicitString: String = assumedString // no ! needed — unwrapped automatically

You can still treat an implicitly unwrapped optional like a normal optional — check it against nil, bind it with if let. But every plain use of it forces an unwrap behind the scenes:

if assumedString != nil {
print(assumedString!)
}
if let definiteString = assumedString {
print(definiteString)
}

Optional chaining: querying through nil

We’ve spent the chapter unwrapping a single optional. But real models nest — a Person has an optional Residence, which has an optional Address, which has an optional street. Optional chaining lets you reach down through all of them in one expression, failing gracefully the moment any link is nil.

Optional chaining is the safe sibling of forced unwrapping. Where ! traps on nil, ? returns nil. Start with two simple classes:

class Person {
var residence: Residence?
}
class Residence {
var numberOfRooms = 1
}

A fresh Person has residence == nil (optionals default to nil). Forcing through it crashes:

let john = Person()
let roomCount = john.residence!.numberOfRooms
// this triggers a runtime error — residence is nil

Swap the ! for ? and the call fails gracefully instead, returning nil:

if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
// Unable to retrieve the number of rooms.

Three flow paths for the chain john.residence?.address?.street. Path 1: residence is nil, so the ? short-circuits at the nil link and the whole expression yields nil. Path 2: every link is populated, yielding .some("Acacia"), which is still String? because chaining flattens rather than stacking into String??. Path 3: using ! at a nil link traps and crashes. The result of a chain is always optional — ? fails gracefully, while ! is a promise that crashes when broken.

Chaining multiple levels deep

You can chain as deep as your model goes. Add an Address with an optional street:

class Residence {
var rooms: [Room] = []
var numberOfRooms: Int { rooms.count }
subscript(i: Int) -> Room { rooms[i] }
var address: Address?
}
class Room {
let name: String
init(name: String) { self.name = name }
}
class Address {
var street: String?
}

Now reach through two optional links at once:

if let johnsStreet = john.residence?.address?.street {
print("John's street name is \(johnsStreet).")
} else {
print("Unable to retrieve the address.")
}
// Unable to retrieve the address.

Chaining on subscripts and methods

The ? goes before a subscript’s brackets — it always follows immediately after the optional part of the expression:

if let firstRoomName = john.residence?[0].name {
print("The first room name is \(firstRoomName).")
} else {
print("Unable to retrieve the first room name.")
}
// Unable to retrieve the first room name.

Calling a method through a chain works even for methods that return nothing. A Void-returning method becomes Void? through a chain — and comparing that against nil tells you whether the call actually happened:

if john.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.")
}
// It was not possible to print the number of rooms.

The same trick works for setting through a chain. The assignment returns Void?; if the chain was nil, nothing on the right-hand side is even evaluated:

let someAddress = Address()
john.residence?.address = someAddress
// residence is nil, so someAddress is never assigned — and the chain silently no-ops

Forced unwrapping says “this is never nil — crash if I’m wrong.” Optional chaining says “this might be nil — and that’s fine, just give me nil back.” Same ?/! duality, all the way down.

Recap

The unwrapping toolbox as a comparison table. Five rows — forced unwrap !, if let, guard let, ??, and optional chaining ?. — each with a safety column (only ! can crash; the other four are safe) and a returns column. !, if let, guard let, and ?? all yield a non-optional T (or, for !, trap if nil), while optional chaining ?. is the only one that returns an optional T?. A use-it-when column says when each applies.

  • Optional — a value that’s either present (.some) or absent (.none / nil); written T?, which is exactly Optional<T>
  • Under the hoodOptional is just an enum with two cases; every unwrap is enum pattern matching
  • nil — the absence of a value, not a null pointer; only optionals can be nil
  • Forced unwrapping (!) — grabs the value but traps if nil; a promise you can break
  • Optional binding (if let / guard let) — safely unwrap into a non-optional constant; guard let keeps it in scope for the rest of the body
  • Nil-coalescing (??) — unwrap-or-default; the default is lazy (@autoclosure) and short-circuits
  • Implicitly unwrapped (T!) — an optional unwrapped automatically on every use; still crashes if nil
  • Optional chaining (?) — query through nested optionals; the result is always optional and the chain flattens and fails gracefully

What’s next

In the next article we explore Error Handlingthrows, try, do/catch, and Result. We’ll see how Swift draws a sharp line between an expected absence of a value (an optional) and an expected failure with a reason (a thrown error), and why try? quietly turns one into the other.

See you next week.

The billion-dollar mistake wasn’t null itself — it was making every reference nullable by default. Swift’s fix is to make absence visible in the type. Once you internalize “an optional is just an enum,” nil stops being scary and starts being honest.

References

Related