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

!Type Narrowing Flow

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 typesstr? can be string or nil - Check before usingif 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.