Debugging: breakpoints, Watch and the Immediate window
Step through code, inspect state with the Locals and Watch windows, and use Debug.Print and Stop to find the bug fast.
The debugging tools
Option Explicit
Sub ToDebug(ByVal data As Variant)
Dim i As Long
Dim total As Double
' Debug.Print goes to the Immediate window, and only there.
' It is far better than MsgBox: it does not block, and it leaves a trace.
Debug.Print "start, rows=" & UBound(data, 1)
For i = 1 To UBound(data, 1)
If VarType(data(i, 1)) = vbDouble Then
total = total + data(i, 1)
Else
' A breakpoint on the next line stops here and shows every value
' in the Locals window without changing the code.
Debug.Print "skipped row " & i & " type " & VarType(data(i, 1))
End If
Next i
Debug.Print "total=" & Format$(total, "#,##0.00")
End Sub
' Assert is compiled out of production: the line only fires when a
' condition you believed impossible is false.
Sub UseAssert(ByVal ws As Worksheet)
Debug.Assert ws Is Not Nothing ' only runs with the VBE open
Debug.Assert ws.Range("A1").Value <> "" ' stops if it is empty
End Sub
' Stop is an unconditional breakpoint that lives in the code, so it works
' even if you forgot to set one. Remove it before shipping.
Sub UseStop(ByVal n As Long)
Dim i As Long
For i = 1 To n
If i = 3 Then Stop
Next i
End Sub| Tool | Where | Use it for |
|---|---|---|
| F9 breakpoint | Any executable line | Stop and inspect |
| Stop statement | In the code | A breakpoint you cannot forget |
| F8 Step Into | The VBE | Enter each called procedure |
| Shift+F8 Step Over | The VBE | Skip a procedure you trust |
| Ctrl+Shift+F8 Step Out | The VBE | Finish the current procedure |
| Locals window | View menu | All variables in scope, live |
| Watch window | View menu | One expression, updated as you step |
| Immediate window | Ctrl+G | Evaluate expressions and change values |
| Call Stack | View menu | Which procedures are currently running |
A conditional breakpoint is set in the Watch window: add a watch on an expression and choose Break When Value Is True. It is the correct tool for stopping on the thousandth iteration without adding counters to the code.
The Immediate window
' With the code paused at a breakpoint, the Immediate window evaluates
' anything in scope, and can change state.
' print a value
' ?total
' Debug.Print UBound(data, 1)
' change a value and continue
' i = 500
' data(1, 1) = 0
' run a statement
' Sheet1.Range("A1").Value = "test"
' Application.Calculate
' check a condition before you write the If
' ?TypeName(data)
' ?IsArray(data)
' inspect an object
' ?Sheet1.Name
' ?ActiveWorkbook.FullName
' ?SomeRange.Address
' find the last used row without leaving the debugger
' ?Sheet1.Cells(Sheet1.Rows.Count, "A").End(xlUp).Row
' a common setup line you will type a hundred times
' Sheet1.Range("A1").Value = Err.Number & ": " & Err.Description
' In the Immediate window, a ? is shorthand for Debug.Print.
' Anything without a ? is executed as a statement.
' Clear the Immediate window with Ctrl+A then Delete, or by right-clicking.- Code typed in the Immediate window runs in the context of the paused procedure, so it can read and write its locals. That is what makes it so much better than a message box for experimentation.
- A
Debug.Printleft in a hot loop is a real performance cost. Search for it before shipping, or guard it with a module-level constant. - The Locals window shows everything in scope but not the contents of a large object graph. Use a Watch on a specific expression for those.
- If a variable shows as
Emptywhen you expected a value, check whether the assignment line actually ran. Step with F8 rather than guessing.
Structured debugging
' A debug switch keeps the diagnostics in the code and out of production.
#Const DEBUG_MODE = True
Sub Traced(ByVal n As Long)
Dim i As Long
For i = 1 To n
#If DEBUG_MODE Then
If i Mod 100 = 0 Then Debug.Print "progress: " & i
#End If
Next i
End Sub
' A central handler that reports where the failure happened.
' ErL is the last line number that ran, and it is only useful if you
' number your lines or set it yourself.
Sub WithContext()
Dim ws As Worksheet
On Error GoTo Fail
Set ws = ThisWorkbook.Worksheets("Data")
ws.Range("A1").Value = 1
Exit Sub
Fail:
Debug.Print "error " & Err.Number & ": " & Err.Description
Debug.Print "source: " & Err.Source
Debug.Print "sheet: " & ws.Name ' ws is valid here: it was set
' log to a file or a sheet instead of a message box in a batch run
MsgBox "Failed: " & Err.Description, vbCritical, "Error " & Err.Number
End Sub
' The most useful single habit: reproduce with the smallest possible input.
' Copy the failing row into a fresh workbook, run the same macro, and the
' problem is usually obvious once the other thousand rows are gone.💡
An error handler that hides the failure is worse than no handler. If all it does is show a message and continue, the workbook is now in an unknown state and the next error will be attributed to the wrong place. Log, restore state, and either retry deliberately or stop.
FAQ
Why does my breakpoint show as a hollow circle and never trigger?
A hollow red circle means the line cannot take a breakpoint, usually because it is a declaration, a comment, or inside a compiled-out
#If False block. Move the breakpoint to an executable statement.How do I debug a macro that runs too fast to see?
Add a
Stop statement or a breakpoint at the point you care about. For a loop, use a Watch with Break When Value Is True rather than printing a thousand lines.Related
Error handling and the classic pitfalls Speed: ScreenUpdating, calculation and array transfers
Last refreshed 2026-09-18.