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.
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:
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
Instead of pattern matching everywhere, Option provides convenient methods for common operations. These methods make working with optional values concise and composable.
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 -- nonebind is also written as >>=. It chains operations that might fail—if any step fails, the whole chain fails.Option in Practice
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 -- noneDesigning with Error Types
Use Option when the absence of a value is expected and unremarkable. Use Except when you need to explain why something failed.
Except: Errors with Context
Except ε α is like Option but carries error information:
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
Like Option, Except supports mapand bind operations. These let you transform success values or chain fallible operations without manual error propagation.
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:
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"← syntax extracts values from Except(or Option). If extraction fails, the whole doblock short-circuits and returns the error.Custom Error Types
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
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
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] -- noneCreate a validator that accepts ages 0–120 and returns a helpful error otherwise.
1def validateAge (n : Nat) : Except String Nat :=2 if n <= 120 then .ok n else .error "age out of range"34#eval validateAge 25 -- ok 255#eval validateAge 200 -- error "age out of range"