Error Handling
Lean uses types to make failure explicit. No null pointers, no exceptions—justOption and Except.
The Problem with Null
In languages with null, any value might secretly be null. This leads to runtime crashes. Lean's type system prevents this entirely.
lean
1-- In many languages: head([]) crashes or returns null2-- In Lean: we must handle the empty case34def safeHead (xs : List α) : Option α :=5 match xs with6 | [] => none7 | x :: _ => some x89#eval safeHead [1, 2, 3] -- some 110#eval safeHead ([] : List Nat) -- noneOption: Maybe There's a Value
Option α represents a value that might be missing:
lean
1inductive Option (α : Type) where2 | none : Option α -- No value3 | some : α → Option α -- Has a value45-- Examples6def maybeNumber : Option Nat := some 427def noNumber : Option Nat := none89-- Access with pattern matching10def showOption (opt : Option Nat) : String :=11 match opt with12 | none => "Nothing"13 | some n => s!"Got {n}"Working with Option
lean
1-- getD: get with default2#eval (some 5).getD 0 -- 53#eval none.getD 0 -- 045-- map: transform inner value6#eval (some 5).map (· * 2) -- some 107#eval none.map (· * 2) -- none89-- bind: chain optional operations10def half (n : Nat) : Option Nat :=11 if n % 2 == 0 then some (n / 2) else none1213#eval (some 10).bind half -- some 514#eval (some 7).bind half -- none15#eval none.bind half -- noneℹ
bind is also written as >>=. It chains operations that might fail—if any step fails, the whole chain fails.Option in Practice
lean
1-- Find an element2def find (p : α → Bool) : List α → Option α3 | [] => none4 | x :: xs => if p x then some x else find p xs56#eval find (· > 5) [1, 3, 7, 2] -- some 77#eval find (· > 10) [1, 3, 7, 2] -- none89-- Dictionary lookup10def lookup (key : String) (pairs : List (String × α)) : Option α :=11 match pairs with12 | [] => none13 | (k, v) :: rest => if k == key then some v else lookup key rest1415def env := [("x", 1), ("y", 2)]16#eval lookup "x" env -- some 117#eval lookup "z" env -- noneExcept: Errors with Context
Except ε α is like Option but carries error information:
lean
1inductive Except (ε α : Type) where2 | error : ε → Except ε α -- Failed with error3 | ok : α → Except ε α -- Succeeded with value45-- Example: division with error message6def divide (x y : Nat) : Except String Nat :=7 if y == 0 8 then Except.error "Division by zero"9 else Except.ok (x / y)1011#eval divide 10 2 -- Except.ok 512#eval divide 10 0 -- Except.error "Division by zero"Working with Except
lean
1-- Pattern matching2def showResult (r : Except String Nat) : String :=3 match r with4 | .error e => s!"Error: {e}"5 | .ok n => s!"Success: {n}"67-- map and bind work similarly to Option8#eval (Except.ok 5 : Except String Nat).map (· * 2) -- ok 109#eval (Except.error "oops" : Except String Nat).map (· * 2) -- error "oops"1011-- Chain fallible operations12def safeSqrt (n : Nat) : Except String Nat :=13 if n < 0 then .error "Negative number"14 else .ok (Nat.sqrt n) -- Simplified1516def compute (x y : Nat) : Except String Nat := do17 let quotient ← divide x y18 safeSqrt quotientDo Notation for Error Handling
The do notation makes chaining fallible operations clean:
lean
1-- Without do notation (messy)2def processWithoutDo (x y z : Nat) : Except String Nat :=3 match divide x y with4 | .error e => .error e5 | .ok a => match divide a z with6 | .error e => .error e7 | .ok b => .ok (b * 2)89-- With do notation (clean)10def processWithDo (x y z : Nat) : Except String Nat := do11 let a ← divide x y12 let b ← divide a z13 return b * 21415#eval processWithDo 100 5 2 -- ok 2016#eval processWithDo 100 0 2 -- error "Division by zero"17#eval processWithDo 100 5 0 -- error "Division by zero"Key Takeaway
The
← syntax extracts values from Except(or Option). If extraction fails, the whole doblock short-circuits and returns the error.Custom Error Types
lean
1-- Define specific errors2inductive ValidationError where3 | tooShort : Nat → ValidationError4 | invalidChar : Char → ValidationError5 | reserved : String → ValidationError67def validateUsername (name : String) : Except ValidationError String :=8 if name.length < 3 then9 .error (.tooShort name.length)10 else if name.any (· == ' ') then11 .error (.invalidChar ' ')12 else if name == "admin" then13 .error (.reserved name)14 else15 .ok nameConverting Between Option and Except
lean
1-- Option to Except2def optionToExcept (opt : Option α) (err : ε) : Except ε α :=3 match opt with4 | none => .error err5 | some a => .ok a67-- Except to Option (loses error info)8def exceptToOption (ex : Except ε α) : Option α :=9 match ex with10 | .error _ => none11 | .ok a => some a1213-- Built-in: Option.toExcept14#check Option.toExceptDeep Dive: Why No Exceptions?
Exceptions in languages like Java or Python are problematic:
- They're invisible in function signatures
- They bypass the type system
- They make reasoning about code harder
- They complicate theorem proving
With Option and Except, failure is:
- Visible in the type:
Except Error Value - Checked by the compiler
- Composable with
donotation - Provable (you can prove code handles all errors)
Common Patterns
lean
1-- Handle all errors uniformly2def orElse (x : Option α) (y : Option α) : Option α :=3 match x with4 | some a => some a5 | none => y67#eval orElse none (some 5) -- some 58#eval orElse (some 3) (some 5) -- some 3910-- Collect successes, ignore failures11def filterMap (f : α → Option β) (xs : List α) : List β :=12 xs.filterMap f1314def parseNat? (s : String) : Option Nat := s.toNat?1516#eval ["1", "hello", "3"].filterMap parseNat? -- [1, 3]1718-- Require all to succeed19def sequence (xs : List (Option α)) : Option (List α) :=20 xs.mapM id2122#eval sequence [some 1, some 2, some 3] -- some [1, 2, 3]23#eval sequence [some 1, none, some 3] -- none