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 to! 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 as str # Can't be nil var nickname as str? # Can be nil var bio as str? # Can be nil
class Main shared 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 as 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
class Main shared def main var nickname as str? = "Bobby" var empty as 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
class Main shared def process_name(input as str?) if input == nil return # Exit early if nil
# From here, input is narrowed to str print input.len # ✅ Safe: know it's str print input.upper() # ✅ Safe
The to! Operator (Unwrap)
Warning: Only use when you're absolutely certain the value isn't nil.
// file: 11_unwrap.zbr
// teaches: unwrap operator // chapter: 11-Nil-Tracking-and-Safety
class Main shared def main var name as str? = "Alice"
# Unwrap: assert it's not nil var safe_name = name to! print safe_name # Now just str
# If it WAS nil, this would crash var empty as str? = nil # var crash = empty to! # ❌ Would panic at runtime
Unwrap with Fallback
// file: 11_unwrap_or.zbr
// teaches: safe unwrapping // chapter: 11-Nil-Tracking-and-Safety
class Main shared def get_user_name(user_id as int) as str? if user_id == 1 return "Alice" return nil
def main var name = get_user_name(1)
# Option 1: Check and use default if name != nil print name else print "Unknown user"
# Option 2: if method exists, unwrapOr # var safe_name = name.unwrapOr("Guest") # print safe_name
Real World: Database Queries
// file: 11_database.zbr
// teaches: nil in realistic scenarios // chapter: 11-Nil-Tracking-and-Safety
class User var id as int var name as str var email as str?
class UserDatabase shared def find_user(user_id as int) as 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 as int) as str? var user = find_user(user_id) if user == nil return nil return user.email
class Main shared 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 as int) as 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 as 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 use x.
Common Mistakes
> ❌ Mistake: Forgetting to check for nil > >
> var email as str? = get_email() > print email.len # ❌ Crash if email is nil! > > > ✅ Better: > > var email as str? = get_email() > if email != nil > print email.len # ✅ Safe >
> ❌ Mistake: Using to! without certainty > >
> var value as str? = get_value() > print value to! # ❌ Crashes if value is nil > > > ✅ Better: > > var value as str? = get_value() > if value != nil > print value # ✅ Safe, or use unwrapOr >
> ❌ Mistake: Assigning nil to non-nullable > >
> var name as str = nil # ❌ str can't be nil > > > ✅ Better: > > var name as 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 as int var name as str var email as str?
class UserDB shared def find_user(id as int) as 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 as int) as str? var user = find_user(user_id) if user == nil return nil return user.email
class Main shared 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
shared def safe_divide(a as float, b as float) as float? if b == 0.0 return nil return a / b
class Main shared 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 as str var bio as str?
class User var id as int var profile as Profile?
class UserService shared def get_user_bio(user_id as int) as 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 as int) as 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
class Main shared 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 — Results for 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 - to! 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 Results and error propagation.