Variables, watches, and evaluate expression
On This Lesson 11 topics
Lesson Overview
Pausing a program is only useful if you know what to look at. When the program stops at a breakpoint, everything it knows is on the table: the value of every variable, the call stack, the line about to run. This lesson is about reading that evidence.
For the detective, the paused program is the crime scene, and the Variables panel, watches, and Evaluate Expression are the magnifying glass. The skill is not looking at everything. It is comparing each value with what you expected and noticing the first one that is wrong.
Why this matters: the debugger does not tell you where the bug is. It shows you values, and you supply the judgment. A developer who can say "sum should be 30 here and it is 20" has already cut the search space in half.
This lesson builds on the previous one (breakpoints and stepping), on the hand-tracing table from BEG-016, and on scope from BEG-014-L04. Panel names and shortcuts below follow IntelliJ IDEA and may differ slightly in your version.
Learning Objectives
By the end of this lesson you will be able to:
- Read the Variables panel, and explain why some variables do not appear there.
- Add a watch, and explain how it differs from looking at the panel.
- Use Evaluate Expression to test an idea on the paused program.
- Compare debugger values with a hand-traced table and find the first mismatch.
- Treat a surprising value as a clue and write a hypothesis instead of editing code.
- Explain why expressions with side effects are dangerous in watches and conditions.
Written Explanation
The Variables panel
When the program is paused, IntelliJ's Variables tab lists the variables visible in the currently selected frame at the current execution point. That is the scope idea from BEG-014-L04 made visible: a variable declared inside the loop body appears only while you are inside it, and a variable from main does not appear while you are paused inside another method (select the main frame in the Frames tab to see it).
For each variable you see the name and the current value. For an array or an object, you can expand it to see its elements or fields. IntelliJ also shows inline values in the editor beside each variable and updates them as you step (on by default, per the documentation).
Remember the rule from the last lesson: the program is stopped before the highlighted line. Values reflect everything up to, but not including, that line.
Compare with what you expected
The panel gives you actual values. The second half of the work is the expected values, and that is where your hand-traced table from BEG-016 comes in. Suppose you trace a loop that sums {10, 20, 30, 40}:
| After pass | i | expected sum |
|---|---|---|
| 1 | 0 | 10 |
| 2 | 1 | 30 |
| 3 | 2 | 60 |
| 4 | 3 | 100 |
Now step through in the debugger. At each pause, compare. The first row where the debugger disagrees with the table is the clue: the bug lives in whatever happened just before that row. Everything before it was working, so you can stop checking it.
Watches
A watch is an expression that the debugger keeps showing, evaluated afresh each time the program pauses. The IntelliJ documentation says watches are evaluated in the context of the selected frame and stay in the project between sessions. You can add one by selecting an expression in the editor and choosing Add to Watches, by right-clicking a variable in the Variables tab, or by typing the expression into the field at the top of the tab.
A watch is more than a variable. It can be any expression: sum / count, prices[i] * 2, name.length(). That makes it ideal for checking a theory: if you suspect integer division, watch sum / count and sum / (double) count side by side, and compare them on each pass.
Evaluate Expression
Evaluate Expression (Alt+F8 according to the IntelliJ documentation) runs a piece of code in the current frame, on the paused program, and shows the result. Unlike a watch it is a one-off: you type something, press Evaluate, read the answer. The documentation says it can include method calls and even short loops, and that it works only while the program is suspended at a breakpoint.
Typical uses: "Is my formula right?" (evaluate (double) total / count and compare it with the program's result), or "what would prices[i + 1] be?" (it might be out of bounds, and you find out without crashing the program).
Be careful: evaluation can change the program
The documentation warns that calling methods during evaluation may change state outside the expression; its example is a method that increments a counter. Because watches are re-evaluated on every step, the side effect repeats. The same warning applies to breakpoint conditions.
So keep watches and conditions to pure expressions: comparisons, arithmetic, reading a variable, calling methods that only return a value. Avoid i++, list.remove(...), or a method that writes a field.
Do not edit to see if it works
A tempting move while paused is to change the code ("maybe it should be <=") and rerun. That is the random-change mistake from Lesson 1 in a comfortable disguise. The debugger shows the first wrong value; ask why it is wrong, predict what you would see if your explanation is right, and look for that in a watch or Evaluate Expression. Only then change the code, once, and verify.
You can set a variable's value while paused (the documentation mentions F2 on a selected variable). That is a legitimate experiment ("if count were 4 here, would the rest be right?"), but it tests a hypothesis; it is not a fix, because the code is unchanged.
Examples
Example 1: find where sum first differs (simple)
int[] prices = {10, 20, 30, 40};
int sum = 0;
for (int i = 1; i < prices.length; i++) {
sum += prices[i];
}
System.out.println("Sum: " + sum);The program prints Sum: 90. You expected 100. The hand-trace above says sum should be 10 after the first pass.
Put a breakpoint on sum += prices[i]; and debug. On the first stop the Variables panel shows i = 1 and sum = 0. The table says that on the first pass i should be 0. That is the first mismatch, found before the bug had affected the answer. Add a watch prices[i]: it shows 20, not 10. Hypothesis: the loop starts at 1, so prices[0] is skipped. Fix: int i = 0. Verify: with the fix a real run gave sum=10, 30, 60, 100 on four passes, ending at 100.
What you learned: the debugger found the bug on the first pass, even though the wrong sum was only visible at the end.
Example 2: watches that test a theory (practical)
int[] scores = {7, 8, 8, 9};
int total = 0;
int count = 0;
for (int i = 0; i < scores.length; i++) {
total += scores[i];
count++;
int avg = total / count;
}Suppose a report says the running average looks "stuck at 7". Put a breakpoint on the last line of the loop body (int avg = total / count;), add two watches, total / count and total / (double) count, and resume a few times. A real run of the same code printed:
pass 0: total=7 count=1 avg=7 exact=7.0
pass 1: total=15 count=2 avg=7 exact=7.5
pass 2: total=23 count=3 avg=7 exact=7.666666666666667
pass 3: total=32 count=4 avg=8 exact=8.0Both watches agree on pass 0, so the first mismatch is on pass 1: 15 / 2 shows 7, but the exact watch shows 7.5. That one comparison confirms that the program does int division (the same family as Lesson 1) and that total and count themselves are right. You have tested a hypothesis without modifying the code, and it tells you which line to fix: make avg a double computed with (double).
Example 3: a watch that changes the program (realistic)
static int calls = 0;
static int next() {
calls++;
return calls;
}A developer adds a watch next() to look at its value. Every time the debugger pauses, the watch runs the method, and calls goes up by one, even though the program did not call next() itself. The program's behavior changes just because someone looked at it, which can create a bug (or hide one) that exists only while debugging. The safer watch is calls, which reads the field and changes nothing.
In a backend, the same trap appears with methods that save data or send a message: a watch that calls one does it for real. Inspect the variables the method uses instead of calling it.
Backend Insight
Backend data is mostly objects: orders, users, requests. The Variables panel lets you expand an object and read its fields without printing. You will often find the bug there: an id of 0, an empty list, a text field that is null.
The habit to take away is "state first": before you theorize about code, look at the data the code received. Many backend bugs are not wrong logic; the logic is fine and the data was not what you assumed. Writing down the expected value before you look makes the surprising ones stand out.
Common Mistakes
Mistake 1: Ignoring the first surprise
If i should be 0 and is 1, stop there. Looking at later values only shows consequences.
Mistake 2: Reading the value before the line has run
The program is stopped before the highlighted line. A variable that line sets still has its old value.
Mistake 3: Looking at the wrong frame
Variables not in scope are not shown. Check which frame is selected in the Frames tab.
Mistake 4: Watching an expression with side effects
Methods that change state corrupt the thing you are observing.
Mistake 5: Editing code before forming a hypothesis
A change made "to see" will cost you the clue you just found.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. Why might the variable total from main not appear in the Variables panel while you are paused inside another method?
Show solution A1
A1. The Variables panel shows variables visible in the selected frame. A variable declared in main is out of scope in another method's frame. Select the main frame in the Frames tab to see it.
A2. What is the difference between a watch and Evaluate Expression?
Show solution A2
A2. A watch is kept in view and re-evaluated at every pause. Evaluate Expression is a one-off calculation you ask for while the program is paused.
A3. A loop should add 5, 10, 15, 20. After the third pass the debugger shows sum = 25. What is the expected value, and does this value tell you anything?
Show solution A3
A3. Expected 5 + 10 + 15 = 30. The value 25 tells you that something was off by 5 by that point: the first row that disagrees (check the earlier passes) points to the line that failed, perhaps a skipped or repeated item.
Level B: Application
B1. This loop should print the sum of the first four even numbers, 0 + 2 + 4 + 6:
int sum = 0;
for (int i = 0; i < 4; i++) {
sum += i;
}Make a hand-trace table (expected sum after each pass if the goal is the sum of the first four even numbers), then say what the debugger would show in i and sum on each pass, and find the first mismatch.
Show solution B1
B1. The expected sum after each pass is 0, 2, 6, 12 (the numbers 0, 2, 4, 6 added in turn; the total is 12). The code adds i, so the debugger shows 0, 1, 3, 6. The first mismatch is on pass 2: expected 2, actual 1. The loop adds i, not the i-th even number. A fix is sum += 2 * i;.
B2. A program computes a percentage with int pct = done * 100 / total;. For done = 1 and total = 3, it shows 33. You expected 33.33. Name two watch expressions you would add to confirm what is going on.
Show solution B2
B2. done * 100 (should be 100) and done * 100 / total (shows 33, int division). Compare with done * 100.0 / total which evaluates to 33.333... This confirms integer division as the cause.
B3. Explain why count++ is a poor choice for a breakpoint condition.
Show solution B3
B3. It changes the program's state every time the condition is evaluated, so the loop count is wrong and the debugger alters the behavior you are trying to observe. The docs warn that conditions may have side effects.
Level C: Challenge
C1. A method average(int[] v) returns sum / v.length as a double, and average({1, 2}) gives 1.0. You pause at the return line. List the watches or evaluations you would use, what each shows if integer division is the cause, and what would reject the theory.
Show solution C1
C1. Watch sum (3) and v.length (2): both correct. Evaluate sum / v.length and expect 1 if integer division is the cause; evaluate (double) sum / v.length and expect 1.5. If sum itself were not 3 or v.length not 2, the theory would be rejected and the bug would be earlier, in the loop. (The return type being double does not matter, since the division happens before conversion.)
C2. You pause in a loop at i = 4 and the program has an array of length 4. Write what you would evaluate and what it tells you, without running the program.
Show solution C2
C2. Evaluate i < values.length (here 4 < 4), which is false, and values[i], which would throw an out-of-bounds error. The valid indexes of a length-4 array are 0 to 3, so the loop condition lets i reach 4 (for example i <= values.length). Evaluating values[i] here would be an out-of-bounds access; expect an error message from the evaluation rather than a crash of your program (I did not verify the exact behavior).
Mini Challenge
Take the Lesson 1 average method with a deliberate integer division bug. First write a table of the expected sum after each pass for the array {1, 2, 4}. Then debug:
- Add a watch for
sum, and compare with your table on each pass. - Pause at the
returnline and add the watchessum / values.lengthand(double) sum / values.length. - Write two lines: the first value that surprised you, and the hypothesis it suggests.
Check your work: you found the first mismatch before looking at the final result, and your hypothesis predicts what the second watch will show.
Summary & Next Lesson
Key takeaways
- When paused, the Variables panel shows the values visible in the selected frame, up to but not including the line about to run.
- Compare each value with your hand-traced expectation; the first mismatch is the clue.
- A watch keeps an expression in view; Evaluate Expression tests an idea once. Both work on the paused program.
- Keep watches and conditions free of side effects.
- Form a hypothesis before editing code.
Next lesson
Not every bug is a wrong answer: sometimes the program crashes and prints a block of text. The next lesson, "Reading stack traces", shows how to read that text and jump to the failing line.
Self-check
- Which line has not run yet when the debugger pauses?
- What do you do with a value that surprises you?
- How is a watch different from Evaluate Expression?
- Why is
next()(a method that increments a counter) a bad watch? - Why compare values with a hand-traced table?
Show answers
- The highlighted line, the one about to execute.
- Treat it as a clue and write a hypothesis about where it went wrong.
- A watch stays in view and updates at every pause; Evaluate Expression is a one-time check.
- Evaluating it changes the program's state each time, so you alter what you are observing.
- The first value that differs from the table points to the line that caused the bug.
References
- JetBrains, Examine a suspended program. Supports the Variables tab (variables visible in the selected frame), watches (frame context, ways to add them), Evaluate Expression (Alt+F8, works only when suspended at a breakpoint), inline values, setting a value with F2, and the side-effect warning.
- JetBrains, Breakpoints. Supports the warning that breakpoint conditions can have side effects.
- The outputs shown (
Sum: 90for the buggy loop, thepass 0topass 3lines, and thei=0 sum=10toi=3 sum=100trace) come from compiling and running the code with JDK 21. The panel contents described in the examples were derived from the code and documentation, not recorded from an IDE session.
Practice
Apply this lesson with the module's lab, exercises and quiz.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.