11: Nil Tracking and Safety
Audience: All
Time: 120 minutes
Prerequisites: 02-Values, 05-Control-Flow, 07-Classes
You'll learn: Nullable types, nil checking, type narrowing, the ! force-unwrap operator, optionals
The Big Picture
Nil (null/nothing) is one of the most common sources of bugs. Zebra's nil tracking forces you to handle it:
"A billion-dollar mistake." — Tony Hoare, on inventing the null reference
Zebra says: Explicit nil is safe. Implicit nil is forbidden.
Nullable Types
Marking Optionality
# file: 11_nullable.zbr
# teaches: nullable types
# chapter: 11-Nil-Tracking-and-Safety
class User
var name: str # Can't be nil
var nickname: str? # Can be nil
var bio: str? # Can be nil
def main()
var user = User()
user.name = "Alice" # ✅ Fine
user.nickname = nil # ✅ Allowed (it's str?)
user.bio = "Developer" # ✅ Can assign string to str?
# This won't compile:
# var empty: str = nil # ❌ str can't be nil
Key point: The ? mark means "this can be nil or the type."
Nil Checking
Safe Access Pattern
# file: 11_nil_check.zbr
# teaches: nil checking
# chapter: 11-Nil-Tracking-and-Safety
def main()
var nickname: str? = "Bobby"
var empty: str? = nil
# Check before using
if nickname != nil
print(nickname) # Safe: known to not be nil
if empty == nil
print("No nickname set")
else
print(empty)
# Two ways to check:
if nickname != nil
print("Has nickname")
if nickname == nil
print("No nickname")
else
print("Has: ${nickname}")
Type Narrowing

After checking, the type is narrowed:
# file: 11_narrowing.zbr
# teaches: type narrowing
# chapter: 11-Nil-Tracking-and-Safety
def process_name(input: str?)
if input != nil
# Inside the block, input is narrowed to str
print(input.len) # ✅ Safe: know it's str
print(input.upper()) # ✅ Safe
Narrowing follows the if x != nil block, not an early return. The early-return
shape — if input == nil: return and then using input below — does not narrow
(the else-branch of == nil is on the not-yet list), so write it as the block above,
or bind with if input as name (next section), or unwrap explicitly with input!.
# an early return still works, with an explicit unwrap after it
def shout(input: str?): str
if input == nil
return ""
return input!.upper()
if x as n — Unwrap-and-Bind
The most idiomatic way to unwrap an optional and use the inner value is the
if x as n binding form. When x: T?, it binds a non-optional n: T in
the then-branch — no separate unwrap required:
# file: 11_if_as.zbr
# teaches: optional unwrap binding form
# chapter: 11-Nil-Tracking-and-Safety
def main()
var maybe_name: str? = "Alice"
if maybe_name as name
# `name` is `str` here — non-optional
print("Hello, ${name}!")
else
print("No name")
This form combines the nil-check, the unwrap, and the binding into a single
statement, and it works both for plain optionals (T?) and for optional
class/union check + unwrap together:
# Combined type check + unwrap:
var maybe_user: User? = lookup()
if maybe_user is User as u
print(u.name) # u is `User` (non-optional)
if x as n is preferred over ! whenever the wrapped value is going to
be used right away — it's safer (no panic possibility) and reads more
naturally.
The ! Operator (Force-Unwrap)
Warning: Only use when you're absolutely certain the value isn't nil.
# file: 11_unwrap.zbr
# teaches: force-unwrap operator
# chapter: 11-Nil-Tracking-and-Safety
def main()
var name: str? = "Alice"
# Unwrap: assert it's not nil
var safe_name = name!
print(safe_name) # Now just str
# If it WAS nil, this would crash
var empty: str? = nil
# var crash = empty! # ❌ Would panic at runtime
Unwrap with Fallback
# file: 11_unwrap_or.zbr
# teaches: safe unwrapping
# chapter: 11-Nil-Tracking-and-Safety
def get_user_name(user_id: int): str?
if user_id == 1
return "Alice"
return nil
def main()
var maybe_name = get_user_name(1)
# Option 1: Check and use default
if maybe_name as name
print(name)
else
print("Unknown user")
# Option 2: `orelse` supplies a fallback when the value is nil
var safe_name = get_user_name(2) orelse "Guest"
print(safe_name) # Guest
Real World: Database Queries
# file: 11_database.zbr
# teaches: nil in realistic scenarios
# chapter: 11-Nil-Tracking-and-Safety
class User
var id: int
var name: str
var email: str?
class UserDatabase
static
def find_user(user_id: int): User?
if user_id == 1
var user = User()
user.id = 1
user.name = "Alice"
user.email = "alice@example.com"
return user
return nil
def find_user_email(user_id: int): str?
var user = find_user(user_id)
if user == nil
return nil
return user.email
def main()
var user = UserDatabase.find_user(1)
if user != nil
print(user.name)
if user.email != nil
print(user.email)
else
print("No email on file")
else
print("User not found")
# Chaining nil checks
var email = UserDatabase.find_user_email(999)
if email != nil
print("Email: ${email}")
else
print("User not found")
Common Patterns
Optional Chaining
def get_user_city(user_id: int): str?
var user = find_user(user_id)
if user == nil
return nil
var address = get_address(user.id)
if address == nil
return nil
return address.city
Guard Clauses
def process(data: str?)
if data == nil
return
if data.len == 0
return
# Now process safely
do_work(data)
If you're new to programming
Nil means "nothing" or "no value." Different languages call it null, None, undefined.
Zebra makes you declare which variables can be nil with
?. This prevents accidental crashes.Type narrowing means the compiler recognizes that after you check
if x != nil, you can safely usex.
Common Mistakes
❌ Mistake: Forgetting to check for nil
var email: str? = get_email() print(email.len) # ❌ Crash if email is nil!✅ Better:
var email: str? = get_email() if email != nil print(email.len) # ✅ Safe
❌ Mistake: Using
!without certaintyvar value: str? = get_value() print(value!) # ❌ Crashes if value is nil✅ Better:
var value: str? = get_value() if value != nil print(value) # ✅ Safe — or use a fallback: print(value orelse "none")
❌ Mistake: Assigning nil to non-nullable
var name: str = nil # ❌ str can't be nil✅ Better:
var name: str? = nil # ✅ Declares it can be nil
Exercises
Exercise 1: Find User Email
Write a function that safely retrieves a user's email:
Solution
class User
var id: int
var name: str
var email: str?
class UserDB
static
def find_user(id: int): User?
if id == 1
var user = User()
user.id = 1
user.name = "Alice"
user.email = "alice@example.com"
return user
return nil
def get_email(user_id: int): str?
var user = find_user(user_id)
if user == nil
return nil
return user.email
def main()
var email = UserDB.get_email(1)
if email != nil
print("Email: ${email}")
else
print("User not found or no email")
Exercise 2: Safe Division
Write a division function that returns nil on error:
Solution
class Calculator
static
def safe_divide(a: float, b: float): float?
if b == 0.0
return nil
return a / b
def main()
var result = Calculator.safe_divide(10.0, 2.0)
if result != nil
print("Result: ${result}")
var bad = Calculator.safe_divide(10.0, 0.0)
if bad != nil
print(bad)
else
print("Cannot divide by zero")
Exercise 3: Nullable Chain
Write a function that navigates nullable fields:
Solution
class Profile
var name: str
var bio: str?
class User
var id: int
var profile: Profile?
class UserService
static
def get_user_bio(user_id: int): str?
var user = find_user(user_id)
if user == nil
return nil
var profile = user.profile
if profile == nil
return nil
return profile.bio
def find_user(id: int): User?
if id == 1
var user = User()
user.id = 1
var profile = Profile()
profile.name = "Alice"
profile.bio = "Developer"
user.profile = profile
return user
return nil
def main()
var bio = UserService.get_user_bio(1)
if bio != nil
print("Bio: ${bio}")
else
print("No bio found")
Next Steps
- → 12-Error-Handling —
throws,raise, andcatchfor error cases - → 14-Contracts — Enforce non-nil invariants
- 🏋 Project-2-HTTP-Server — Handle nil API responses
Key Takeaways
?marks nullable types —str?can be string or nil- Check before using —
if value != nil { ... } - Type narrowing — Compiler recognizes after checks
!unwraps — Only when certain it's not nil- Guard clauses — Exit early if nil
- Nil safety prevents crashes — It's a feature, not a limitation
Next: Head to 12-Error-Handling for exceptions (throws/raise/catch) and error propagation.