Debugging & Troubleshooting
Learn essential techniques for understanding and fixing errors in Lean. From reading error messages to using #check and #eval, these tools will help you debug code effectively.
Reading Error Messages
Lean's error messages are detailed and helpful once you learn to read them. They typically include the location, what was expected, and what was found:
1-- Example error2def add (x : Nat) (y : Nat) : String := x + y3-- ERROR: type mismatch4-- x + y5-- has type6-- Nat7-- but is expected to have type8-- String910-- The fix: change return type or convert11def add' (x : Nat) (y : Nat) : Nat := x + y12def addStr (x : Nat) (y : Nat) : String := toString (x + y)Using #check
The #checkcommand shows the type of any expression. Use it to understand what you're working with:
1-- Check types of values2#check 42 -- Nat3#check "hello" -- String4#check true -- Bool56-- Check types of functions7#check Nat.add -- Nat.add : Nat → Nat → Nat8#check List.map -- List.map {α : Type u_1} {β : Type u_2} (f : α → β) (l : List α) : List β910-- Check complex expressions11#check (fun (x : Nat) => x + 1) -- Nat → Nat12#check [1, 2, 3].map -- (Nat → ?m) → List ?m (β not yet known)1314-- Check with explicit types15#check (42 : Int) -- Int16#check ([] : List String) -- List StringUsing #eval
#eval runs code and shows the result. Great for testing expressions interactively:
1-- Test computations2#eval 2 + 3 -- 53#eval [1, 2, 3].length -- 34#eval "hello".toUpper -- "HELLO"56-- Test functions7def double (n : Nat) := n * 28#eval double 5 -- 10910-- Test complex expressions11#eval [1, 2, 3].map (· * 2) |> List.sum -- 121213-- #eval CAN run IO — the output goes to the infoview14#eval IO.println "hi" -- prints: hi1516-- ...including a whole do block17#eval do18 IO.println "step 1"19 IO.println "step 2"20 return 42#eval cannot do is print a value whose type has no Repr or ToStringinstance — functions, most of all. #eval (· + 1)fails with could not synthesize a Repr instance, not because evaluation failed but because Lean does not know how to show you the answer. Apply the function to something, or use #check.Using #print
#print shows the full definition of a name. Useful for understanding library functions:
1-- See how things are defined2#print Nat.add3#print List.map4#print Bool56-- Great for understanding type class instances.7-- Trick: ask for the instance first, then print the name it reports.8#check (inferInstance : BEq Nat)9#print instBEqOfDecidableEq -- how Nat's BEq is actually built1011-- Related commands12#print axioms List.map -- what a definition/proof depends on13#print equations Nat.add -- the defining equations, as simp can use themMaking Lean Show More
Half of debugging is getting Lean to stop hiding things. Pretty-printing options turn implicit arguments, coercions, and instances back on:
1-- Show every implicit argument and instance2set_option pp.explicit true in3#check @List.map45-- Show the type of every numeral (great for Nat-vs-Int confusion)6set_option pp.numericTypes true in7#check (42 : Int) -- (42 : Int) : Int89-- Show coercions that are normally invisible10set_option pp.coercions true in11example (n : Nat) : (n : Int) = n := rfl1213-- The blunt instrument: show absolutely everything14set_option pp.all true in15#check 1 + 11617-- Scope it to one command with "in", or to the rest of the file18-- by writing set_option ... on its own line.set_option pp.explicit true almost always reveals the difference — usually a different instance or a hidden coercion.dbg_trace and assert!
For pure functions you cannot sprinkle IO.printlncalls. dbg_trace is the escape hatch: it prints when the expression is evaluated, and is invisible to the type system.
1def f (n : Nat) : Nat :=2 dbg_trace s!"f called with {n}"3 n * 245#eval f 36-- f called with 37-- 689-- assert! checks a condition at runtime and panics if it fails10#eval assert! 1 + 1 == 2; "ok" -- "ok"1112-- #check_failure asserts that something does NOT typecheck.13-- Useful in teaching material and regression tests.14#check_failure (42 : String)dbg_trace fires during evaluation, not during elaboration, and it is skipped entirely inside proofs. It is a debugging aid for programs, not for tactics — for those, use trace_state below.Common Errors and Fixes
Type Mismatch
1-- Problem: wrong type2-- def f : String := 4234-- Fix 1: Change the annotation5def f : Nat := 4267-- Fix 2: Convert the value8def g : String := toString 42Unknown Identifier
1-- Problem: name not in scope2-- #eval unknownFunction34-- Common causes:5-- 1. Typo in the name6-- 2. Missing import (imports MUST be at the very top of the file)7-- 3. Right function, wrong namespace89-- Fix 3a: use the fully qualified name10#eval List.head? [1, 2] -- some 11112-- Fix 3b: open the namespace, for one command or for the rest of the file13open List in14#eval head? [1, 2] -- some 11516-- Fix 3c: dot notation resolves the namespace from the type — usually best17#eval [1, 2].head? -- some 1import lines must come before everything else in a file — before the first def, before any comment-free code. If you add import Mathlibhalfway down, Lean reports a parse error on the import, not on the name you were trying to fix.Function Expected
1def add (x y : Nat) : Nat := x + y23-- Problem: applying a non-function4-- #eval 42 5 -- Error: function expected, 42 is a Nat56-- The usual real cause is missing parentheses. Application binds tighter7-- than any operator, so this is read as ((add 1 2) 3) — three arguments8-- handed to a two-argument function.9-- #eval add 1 2 31011-- Fix: parenthesise what you meant12#eval (add 1 2) + 3 -- 613#eval add 1 (2 + 3) -- 61415-- Same trap with negative numbers — the minus is read as subtraction:16-- #eval add 1 -217-- failed to synthesize HSub (Nat → Nat) Nat ?m ← i.e. (add 1) - 218#eval add 1 (-2 : Int).toNat -- 1Failed to Synthesize Instance
1-- Problem: type class instance not found2def eq [BEq α] (x y : α) := x == y3-- #eval eq (fun (_ : Nat) => 1) (fun _ => 1)4-- Error: failed to synthesize BEq (Nat → Nat) — functions have no decidable equality56-- Fix: use a type that has the instance7#eval eq (5 : Nat) 5 -- true89-- Read the message as a question: "does this type support this operation?"10-- failed to synthesize Repr X → #eval cannot display an X11-- failed to synthesize OfNat X 42 → 42 is not a valid X literal12-- failed to synthesize Decidable p → p cannot be turned into a Bool13-- failed to synthesize Inhabited X → X might be empty, so no default value1415-- Diagnose by asking for the instance directly:16#check (inferInstance : BEq Nat) -- ok17-- #check (inferInstance : BEq (Nat → Nat)) -- fails, and says exactly whyDebugging Proofs: trace and trace_state
Inside a proof, trace prints a message and trace_state prints the current goal and hypotheses. Both are no-ops for the proof itself — they only talk to you.
1example (n : Nat) : n + 0 = n := by2 trace "about to simp"3 trace_state4 -- n : Nat5 -- ⊢ n + 0 = n6 simp simp followed by trace "done"fails with no goals. To see the state after a step, put trace_state before the next step, or just move your cursor there and read the infoview.For seeing what a tactic did internally, the trace. options are more informative than any print:
1-- Which simp lemmas actually fired?2set_option trace.Meta.Tactic.simp true in3example (n : Nat) : n + 0 = n := by simp45-- Why did instance search fail?6set_option trace.Meta.synthInstance true in7#check (inferInstance : BEq Nat)89-- simp? / says: get simp to print the exact "simp only [...]" it used,10-- so you can paste it back and make the proof fast and stable.11example (n : Nat) : n + 0 = n := by simp?The sorry Tactic
sorry lets you skip parts of a proof temporarily. Useful for exploring structure:
1-- Sketch a proof structure, fill in later2theorem complex_proof (n : Nat) : n + 0 = n ∧ 0 + n = n := by3 constructor4 · sorry -- First part5 · sorry -- Second part67-- Warning: sorry makes proofs invalid!8-- Lean shows a warning when sorry is usedsorry introduces the axiom sorryAx, from which anything follows. The danger is not the sorry you can see — it is the one three files away that some theorem you are using quietly depends on. Lean warns at the definition site only.1theorem shaky : 1 = 2 := by sorry -- warning: declaration uses 'sorry'23-- No warning here! The dependency is invisible at the use site.4theorem builtOnSand : 2 = 1 := shaky.symm56-- This is how you catch it:7#print axioms builtOnSand8-- 'builtOnSand' depends on axioms: [sorryAx]910-- A clean result looks like this:11theorem solid : 1 + 1 = 2 := rfl12#print axioms solid13-- 'solid' does not depend on any axioms#print axioms yourMainTheorem the last line of any file you care about. Seeing only propext, Classical.choice, and Quot.soundis normal and fine — those are the three standard axioms of Lean's logic. Seeing sorryAx is not.Best Practices
- Use #check liberally — Type confusion is the #1 source of errors
- Test small pieces — Use #eval to test functions before using them
- Read error messages fully — They often contain the solution
- Use sorry to sketch — Then fill in one piece at a time
- Check imports— Many "unknown identifier" errors are missing imports
Fix the type error in this code:
1-- This has an error2-- def greet (name : String) : Nat := "Hello, " ++ name34-- Fixed version5def greet (name : String) : String := "Hello, " ++ name