Zulip Chat Archive
Stream: lean4
Topic: Disappearing error message.
Patrick Massot (Feb 10 2026 at 20:43):
The following code contains two variants of https://github.com/leanprover/lean4/blob/master/src/Lean/Elab/Tactic/Calc.lean#L19. The first one simply removes some code that is not relevant to the question. Unsurprisingly the corresponding Calc tactic show an error message when there is an unknown variable in the code. Then the Balc tactic has exactly the same code but wrapped into a try … catch e => throw e. This time the error message disappears and we only get a sorry. How could I fix that?
import Lean
open Lean Elab Tactic Meta
syntax "Calc" calcSteps : tactic
elab_rules : tactic
| `(tactic| Calc%$tk $steps:calcSteps) =>
withRef tk do
closeMainGoalUsing `calc (checkNewUnassigned := false) fun target tag => do
withTacticInfoContext steps do
let steps ← Term.mkCalcStepViews steps
let target := (← instantiateMVars target).consumeMData
let (val, mvarIds) ← withCollectingNewGoalsFrom (parentTag := tag) (tagSuffix := `calc) <| runTermElab do
let (val, valType) ← Term.elabCalcSteps steps
if (← isDefEq valType target) then
-- Immediately the right type, no need for further processing.
return val
Term.ensureHasTypeWithErrorMsgs target val
(mkImmedErrorMsg := fun _ => Term.throwCalcFailure steps)
(mkErrorMsg := fun _ => Term.throwCalcFailure steps)
pushGoals mvarIds
return val
/-- error: Unknown identifier `x` -/
#guard_msgs in
example : 1 = 1 := by
Calc x = 1 := rfl
_ = 1 := rfl
syntax "Balc" calcSteps : tactic
elab_rules : tactic
| `(tactic| Balc%$tk $steps:calcSteps) =>
withRef tk do
try
closeMainGoalUsing `calc (checkNewUnassigned := false) fun target tag => do
withTacticInfoContext steps do
let steps ← Term.mkCalcStepViews steps
let target := (← instantiateMVars target).consumeMData
let (val, mvarIds) ← withCollectingNewGoalsFrom (parentTag := tag) (tagSuffix := `calc) <| runTermElab do
let (val, valType) ← Term.elabCalcSteps steps
if (← isDefEq valType target) then
-- Immediately the right type, no need for further processing.
return val
Term.ensureHasTypeWithErrorMsgs target val
(mkImmedErrorMsg := fun _ => Term.throwCalcFailure steps)
(mkErrorMsg := fun _ => Term.throwCalcFailure steps)
pushGoals mvarIds
return val
catch e => throw e
/-- warning: declaration uses 'sorry' -/
#guard_msgs in
example : 1 = 1 := by
Balc x = 1 := rfl
_ = 1 := rfl
Patrick Massot (Feb 10 2026 at 20:43):
Of course in my real code I’d like to try more things in the catch part, but still being able to throw e (and get the error message) when it fails.
Thomas Murrills (Feb 10 2026 at 21:58):
What's going on here is that Term.throwCalcFailure is merely logging the informative error that you want to keep, and the actual error it throws (which is caught by catch) is an internal error, from e.g. throwAbortTerm. If you write catch e => logInfo m!"{e.toMessageData}"; throw e instead, you can see from the logged message that e is internal exception #4. That is, e does not contain these errors you see; the messages in the tactic state do, and try/catch is rewinding that state when it catches the error.
I believe you might have to resort to docs#Lean.Elab.Tactic.tryCatch, which is just a regular function that does non-backtracking try/catch. Then you can manage state manually like so (replace the Balc elab_rules in your code above):
elab_rules : tactic
| `(tactic| Balc%$tk $steps:calcSteps) =>
withRef tk do
let s ← saveState
Tactic.tryCatch (do
-- same code...
closeMainGoalUsing `calc (checkNewUnassigned := false) fun target tag => do
withTacticInfoContext steps do
let steps ← Term.mkCalcStepViews steps
let target := (← instantiateMVars target).consumeMData
let (val, mvarIds) ← withCollectingNewGoalsFrom (parentTag := tag) (tagSuffix := `calc) <| runTermElab do
let (val, valType) ← Term.elabCalcSteps steps
if (← isDefEq valType target) then
-- Immediately the right type, no need for further processing.
return val
Term.ensureHasTypeWithErrorMsgs target val
(mkImmedErrorMsg := fun _ => Term.throwCalcFailure steps)
(mkErrorMsg := fun _ => Term.throwCalcFailure steps)
pushGoals mvarIds
return val)
-- ...until here.
(fun e₁ => do
-- save the state created by the failure, which has error messages
let failureState ← saveState
try
s.restore -- restore the initial state to start fresh
let success := false
unless success do throwError "Oh no, a second time!"
catch _e₂ => do
-- in the case of a second failure restore state of first failure and throw that error
failureState.restore; throw e₁)
You may need to make your nested try/catch a Tactic.tryCatch if you want to save state from there as well! And you may want to do things like merge your two exceptions or messages.
Technically the second try/catch will save and restore failureState automatically. But I wanted to make it clear what's going on, and give you the chance to adjust it to another Tactic.tryCatch if you need to.
Note also that this is partly because you're in TacticM. You might be aware that there's quite unfortunate inconsistency among do notation try/catch: TacticM's try/catch backtracks, restoring the state, but in other monads like MetaM it won't.
Thomas Murrills (Feb 10 2026 at 22:01):
(EDIT: hang on, I think I copied the wrong code, or made an edit somewhere...) ah, nevermind, all is well.
Patrick Massot (Feb 11 2026 at 09:05):
Thanks a lot Thomas! I had partly understood what was going on but couldn’t fix it. Your solution also seems to work in my real code.
Last updated: Feb 28 2026 at 14:05 UTC