Chapter 21: The Debugger - Your New Best Friend
The Smalltalk Debugger is not like debuggers in other languages. It’s not just a tool for finding bugs - it’s a live development environment where you write code, fix errors, and continue execution without restarting your program.
Many developers say the Debugger is Smalltalk’s “killer feature” - the tool that makes Smalltalk development fundamentally different and more productive than other environments.
In this chapter, you’ll learn to use the Debugger not just for fixing bugs, but as a primary development tool. By the end, you’ll wonder how you ever programmed without it!
What Makes Smalltalk’s Debugger Special?
In traditional languages:
1. Code throws an error
2. Program crashes or prints stack trace
3. You read the trace
4. You edit the source file
5. You recompile
6. You re-run the entire program
7. Hope it works this time
In Smalltalk:
1. Code throws an error
2. Debugger opens, showing the exact execution point
3. You see all variables and their values
4. You fix the method RIGHT THERE in the Debugger
5. You continue execution from where it stopped
6. The program keeps running with the fix applied
No restart. No recompile. No losing state. Just fix and continue.
How the Debugger Opens
The Debugger opens when an error occurs:
Automatic Opening
Try this in the Playground:
1 / 0
Execute it (Ctrl+D / Cmd+D).
Boom! A Debugger window opens with the error: ZeroDivide: attempt to divide by zero
Manual Opening
You can also explicitly open the Debugger:
self halt
This sets a breakpoint. When execution reaches halt, the Debugger opens.
Error Handling
When any unhandled error occurs, the Debugger opens automatically. This includes:
ZeroDivideMessageNotUnderstoodSubscriptOutOfBounds- Custom errors you signal
The Debugger Interface
The Debugger has several panes:
1. Error Message (Top)
Shows what went wrong:
ZeroDivide: attempt to divide by zero
2. Stack Trace (Top-Left)
Shows the call stack - the sequence of method calls that led to the error:
UndefinedObject >> DoIt
SmallInteger >> /
...
Each line is a method call. The topmost is where the error occurred.
3. Context Pane (Top-Right)
Shows local variables and their values for the selected stack frame:
self- The receiver- Method parameters
- Temporary variables
4. Code Pane (Bottom)
Shows the source code of the selected method, with the current execution point highlighted (often with a yellow arrow or marker).
A Simple Debugging Session
Let’s debug a real error:
Step 1: Create Buggy Code
In the Playground:
| numbers |
numbers := #(1 2 3 4 5).
numbers at: 10
Execute. Error! SubscriptOutOfBounds: 10 is out of bounds
The Debugger opens.
Step 2: Examine the Stack
Look at the stack trace:
UndefinedObject >> DoIt
Array >> at:
Array >> at:ifAbsent:
...
Click on Array >> at:. The code pane shows the at: method implementation.
Step 3: Examine Variables
In the Context pane, see:
self- the array#(1 2 3 4 5)index-10
The problem is clear: trying to access index 10 in an array of size 5!
Step 4: Fix
The fix is in your code (the Playground), not in Array. Close the Debugger and fix:
| numbers |
numbers := #(1 2 3 4 5).
numbers at: 3 "Valid index!"
Debugging Your Own Code
Let’s debug code you wrote:
Step 1: Create Buggy Code
Create a Calculator class:
Object subclass: #Calculator
instanceVariableNames: 'result'
classVariableNames: ''
package: 'MyApp'
Add a buggy method:
initialize
result := 0
divide: number
result := result / number.
^ result
Step 2: Trigger the Bug
| calc |
calc := Calculator new.
calc divide: 5. "Works: 0 / 5 = 0"
calc divide: 0. "Error!"
Debugger opens: ZeroDivide: attempt to divide by zero
Step 3: Examine
Stack shows:
Calculator >> divide:
SmallInteger >> /
...
Click on Calculator >> divide:. See the code:
divide: number
result := result / number. ← Error here
^ result
Context pane shows:
self- the Calculatornumber- 0result- 0
You’re dividing result (0) by number (0). That’s the bug!
Step 4: Fix It
Here’s where it gets amazing: Fix the method RIGHT IN THE DEBUGGER!
In the code pane, edit the method:
divide: number
number = 0 ifTrue: [ self error: 'Cannot divide by zero' ].
result := result / number.
^ result
Step 5: Accept (Save)
Press Ctrl+S / Cmd+S to accept the change.
The method is now fixed! It’s compiled into the Image.
Step 6: Continue or Restart
Now you have options:
- Proceed - Continue execution from where it stopped
- Restart - Re-run the current method with the new code
- Step - Execute one line at a time
- Into - Step into method calls
Click Restart. The method runs again with the new code!
Now it throws your custom error: Cannot divide by zero. Much better!
Stepping Through Code
The Debugger lets you execute code line by line:
Buttons/Commands:
- Step Over (or
Overbutton) - Execute current line, don’t enter method calls - Step Into (or
Intobutton) - Execute current line, enter method calls - Step Through - Like Step Into, but more aggressive
- Restart - Re-run the current method
- Proceed - Continue normal execution
- Return Value - Return early from the method
Example
Object subclass: #Greeter
instanceVariableNames: 'name'
...
greet
| greeting |
greeting := self buildGreeting.
^ greeting
buildGreeting
^ 'Hello, ' , name , '!'
Set a breakpoint:
greet
| greeting |
self halt. "Breakpoint!"
greeting := self buildGreeting.
^ greeting
Execute:
| greeter |
greeter := Greeter new name: 'Alice'.
greeter greet
Debugger opens at self halt.
Step Over:
- Click
Step Over - Executes
greeting := self buildGreeting - You see
greetingnow has the value'Hello, Alice!' - Click
Step Overagain - Executes
^ greeting - Method returns
Step Into:
- Click
Step Intoinstead - Enters
buildGreetingmethod - You see the code:
^ 'Hello, ' , name , '!' - Click
Stepagain - Executes the concatenation
- Method returns to
greet
This lets you trace execution line by line!
Inspecting Variables
While debugging, you can inspect any variable:
In the Context pane, right-click on a variable → Inspect.
An Inspector opens showing that variable’s value and structure!
This combines the Inspector (Chapter 20) with the Debugger.
Modifying Variables
You can change variable values while debugging:
In the Debugger Code Pane:
greeting := 'Goodbye, cruel world!'
Execute this line (Ctrl+D / Cmd+D).
Now greeting has a new value! Continue execution and it uses the new value.
Fixing Code Without Restarting
This is the game-changer. A realistic scenario:
Scenario: Long-Running Process
1 to: 1000000 do: [ :i |
self processItem: i.
i = 500000 ifTrue: [ self error: 'Oops!' ] ]
This processes a million items. At item 500,000, it errors.
Traditional debugger: Start over. Process 500,000 items again. Slow!
Smalltalk Debugger:
- Error occurs at 500,000
- Debugger opens
- You see the problem
- Fix the method
- Click
RestartorProceed - Processing continues from 500,000!
No restart. No re-processing. Just fix and continue.
Writing Code in the Debugger
Some Smalltalkers write most code in the Debugger:
The Workflow:
- Write a skeleton method:
processOrder: order self notYetImplemented - Execute it:
processor processOrder: myOrder -
Debugger opens:
notYetImplemented - Implement the method in the Debugger:
processOrder: order | total | total := order calculateTotal. self validateTotal: total. self chargeCustomer: order customer amount: total. ^ order -
Accept the method.
-
Now
validateTotal:doesn’t exist. Debugger opens again. -
Implement
validateTotal:in the Debugger. - Continue!
This is called Debugging-Driven Development. You write code incrementally, in the context where it runs, with live data.
MessageNotUnderstood
A common error: calling a method that doesn’t exist.
'hello' yell
Error: MessageNotUnderstood: ByteString>>yell
The Debugger opens. In the Code pane, you see where the error occurred.
Fix It:
- The Debugger often has a
Createbutton - Click it to create the missing method
- Choose the class:
String - Implement:
yell ^ self asUppercase , '!!!' - Accept
- Click
ProceedorRestart
The method is now defined and execution continues!
Halt and Breakpoints
Explicit Halt
processData: data
self halt. "Breakpoint here!"
result := data collect: [ :each | each * 2 ].
^ result
When execution reaches self halt, the Debugger opens. You can inspect variables, step through, etc.
Conditional Halt
processData: data
data size > 1000 ifTrue: [ self halt ]. "Only halt on large data"
result := data collect: [ :each | each * 2 ].
^ result
Halt Once
self haltOnce
Halts the first time it’s reached, then disables itself. Great for loops!
The Stack is Live
The stack in the Debugger is live - it’s the actual execution stack, not a copy.
You can:
- Modify variables in any stack frame
- Restart any method
- Return from any method early
Example:
- Method A calls Method B calls Method C
- C errors
- Debugger shows stack: A → B → C
- Click on B (middle of stack)
- Modify variables in B
- Restart B
- B re-executes with new values
This is incredibly powerful for testing what-if scenarios!
Debugging Blocks
Blocks can be tricky to debug, but the Debugger handles them:
numbers := #(1 2 3 4 5).
numbers do: [ :n |
n = 3 ifTrue: [ self halt ].
Transcript show: n printString; cr ]
The Debugger opens when n = 3. You can see:
n- 3numbers- the array- The block context
Step through the block’s code!
Debugging Unit Tests
When a unit test fails, the Debugger opens automatically (in test mode):
testAddition
| result |
result := calculator add: 2 to: 2.
self assert: result equals: 5 "Wrong! Should be 4"
The Debugger opens showing the assertion failure. You can:
- See why it failed (
resultis 4, not 5) - Fix the test or the code
- Re-run the test
Advanced: Proceed and Return
Proceed
Click Proceed to continue normal execution after an error. Use this if you fixed the problem and want to keep going.
Return Value
Want to skip a method and return a specific value?
In the Debugger code pane:
^ 42
Execute (Ctrl+D / Cmd+D). The method returns 42 immediately!
Or right-click on a stack frame → Return entered value → Enter a value.
Debugging Tips
Tip 1: Don’t Fear Errors
Errors are opportunities! They open the Debugger where you can fix things.
Tip 2: Use halt Liberally
Drop self halt anywhere to pause execution and explore.
Tip 3: Inspect Everything
Right-click on variables in the Context pane and inspect them.
Tip 4: Restart Methods
Made a change? Click Restart to re-run the method with the new code.
Tip 5: Write Code in the Debugger
Don’t pre-write everything. Let errors guide you to what needs implementing.
Tip 6: Read the Stack
The stack trace tells a story. Follow it from top to bottom to understand how you got here.
Tip 7: Test Assumptions
Not sure what a variable is? Execute code in the Debugger:
myVar class.
myVar inspect.
myVar printString
Common Debugging Scenarios
Scenario 1: Nil Reference
person address city
Error: MessageNotUnderstood: UndefinedObject>>city
Meaning: address returned nil, and you tried to send city to nil.
Fix: Check for nil:
person address ifNotNil: [ :addr | addr city ]
Scenario 2: Wrong Variable Value
A variable has an unexpected value. Inspect it in the Debugger. Trace back through the stack to see where it was set incorrectly.
Scenario 3: Infinite Loop
Code loops forever. Press Ctrl+. (or Cmd+.) to interrupt. The Debugger opens mid-loop. Examine variables to see why the loop won’t terminate.
Scenario 4: Performance Issue
Code is slow. Use self halt to pause at various points. Inspect variables to see if data structures are too large or algorithms inefficient.
Keyboard Shortcuts
- Ctrl+. / Cmd+. - Interrupt execution (opens Debugger)
- Over - Step over
- Into - Step into
- Through - Step through
- Restart - Restart current method
- Proceed - Continue execution
- Ctrl+S / Cmd+S - Accept method changes
- Ctrl+I / Cmd+I - Inspect variable
Debugging Mindset
Traditional Debugging:
- Error occurs
- Panic!
- Add print statements
- Re-run
- Guess at the problem
- Try a fix
- Re-run
- Repeat
Smalltalk Debugging:
- Error occurs
- Debugger opens
- Examine state
- Understand problem
- Fix it right there
- Continue
- Done!
Much faster. Much less stressful.
Example: Full Debugging Session
Let’s debug a realistic problem:
Code:
Object subclass: #ShoppingCart
instanceVariableNames: 'items'
...
initialize
super initialize.
items := OrderedCollection new
addItem: item
items add: item
total
| sum |
sum := 0.
items do: [ :item | sum := sum + item price ].
^ sum
Object subclass: #Product
instanceVariableNames: 'name price'
...
name: aName price: aPrice
name := aName.
price := aPrice.
^ self
Usage:
| cart |
cart := ShoppingCart new.
cart addItem: (Product new name: 'Book' price: 15).
cart addItem: (Product new name: 'Pen' price: 2).
cart addItem: (Product new name: 'Notebook'). "Oops, forgot price!"
cart total
Error! MessageNotUnderstood: UndefinedObject>>#+
Debugging:
-
Debugger opens showing
UndefinedObject>>#+ -
Stack trace shows:
ShoppingCart>>totalcalled+onnil - Context pane shows:
sum- 17item- a Product (the Notebook)
- Inspect
item: Right-click → Inspect. See:name- ‘Notebook’price-nil(Aha!)
-
Root cause: Product’s price is nil because we didn’t set it!
- Fix #1 - Defensive: Edit
totalin the Debugger:total | sum | sum := 0. items do: [ :item | item price ifNotNil: [ :p | sum := sum + p ] ]. ^ sumAccept. Click
Restart. Now it works, returning 17 (skipping the nil-price item). - Fix #2 - Proper: Also fix Product to have a default price:
initialize super initialize. name := ''. price := 0 "Default price"
Fixed! And you did it all without restarting the program!
Try This!
Practice debugging:
- Trigger simple errors:
1 / 0. 'hello' at: 100. nil sizeOpen the Debugger. Examine the stack and variables.
- Use
halt:1 to: 10 do: [ :i | i = 5 ifTrue: [ self halt ]. Transcript show: i printString; cr ]The Debugger opens at 5. Inspect
i. Step through the loop. - Fix a method in the Debugger:
Object subclass: #BuggyClass instanceVariableNames: '' ... buggyMethod ^ 1 / 0 "Intentionally buggy!"Execute:
BuggyClass new buggyMethodDebugger opens. Fix the method to return 42. Accept. Restart. Success!
- Create a missing method:
'hello' rot13Debugger opens:
MessageNotUnderstood. Create therot13method in the Debugger:rot13 "Simple rot13 cipher" ^ self collect: [ :char | (char isLetter) ifTrue: [ ... ] "Implement ROT13 logic" ifFalse: [ char ] ] - Debug nested calls:
methodA ^ self methodB methodB ^ self methodC methodC self halt. ^ 42Execute
object methodA. Debugger opens inmethodC. Look at the stack: A → B → C. Click on each to see how you got here. - Modify and continue:
| count | count := 0. 1 to: 10 do: [ :i | count := count + 1. i = 5 ifTrue: [ self halt ] ]. countDebugger opens at 5. In the code pane, execute:
count := 100. ClickProceed. Final count is 105!
Common Mistakes
Closing the Debugger Too Soon
Don’t panic-close the Debugger! Examine what went wrong first.
Not Using Restart
After fixing a method, use Restart to re-run it, not Proceed.
Editing Without Accepting
Edit the code but forget to press Ctrl+S / Cmd+S. Your changes aren’t saved!
Fear of the Debugger
Embrace errors! They’re learning opportunities.
The Philosophy
The Debugger embodies Smalltalk’s core philosophy:
Live Programming
You’re not editing dead text files. You’re modifying a running system.
Immediate Feedback
See results instantly. No compile-run cycle.
Exploration
The Debugger encourages exploration. Poke around! Try things!
Safety
Mistakes aren’t fatal. The Debugger catches them and lets you fix them.
Looking Ahead
You now understand the Debugger - one of Smalltalk’s most powerful and distinctive tools! You can:
- Debug errors when they occur
- Step through code line by line
- Fix methods while debugging
- Continue execution without restarting
- Write code incrementally in the Debugger
- Inspect variables and modify them
- Create missing methods on the fly
In Chapter 22, we’ll explore the Finder and Spotter - tools for quickly finding code, navigating the system, and discovering methods. These complete your tool toolkit!
Part VI has revealed why Smalltalk developers are so productive: the tools are designed around the living system, enabling a fluid, interactive workflow impossible in traditional environments.
Key Takeaways:
- The Debugger opens when errors occur
- Shows the execution stack, variables, and source code
- You can fix methods directly in the Debugger
- Restart re-runs a method with the new code
- Proceed continues normal execution
- Step Over/Into executes code line by line
- Use
self haltto set breakpoints - The stack is live - you can modify any frame
MessageNotUnderstooderrors let you create missing methods- The Debugger combines code editing, execution, and inspection
- Debugging-Driven Development: Write code incrementally in the Debugger
- No need to restart the program after fixes
- Interrupt with
Ctrl+./Cmd+. - Inspect variables by right-clicking in the Context pane
- The Debugger is a primary development tool, not just for bugs
- Embrace errors - they guide development!
| Previous: Chapter 20 - The Inspector and Explorer | Next: Chapter 22 - The Finder - Discovering Code |