Lecture 14: Program Understanding & Debugging
Situate debugging within the software development lifecycle (5 minutes)
Debugging is not a separate phase of software development—it's an integral part of the Implementation and Validation phases. When we encounter unexpected behavior, we need to understand:
- What the program is actually doing (Program Understanding)
- What the program should be doing (Requirements & Design)
- Why there's a mismatch (Debugging)
This lecture focuses on systematic approaches to program understanding and debugging, building on the design and testing principles we've established throughout the course.
Utilize control flow and data flow analysis to understand a program (10 minutes)
Before we can debug a program, we need to understand what it's actually doing. Two fundamental techniques help us reason about program behavior:
Control flow analysis: Understanding program execution paths
What is control flow? Control flow describes the order in which statements execute. Understanding control flow helps you:
- Identify which code paths are actually executed
- Determine which branches are taken
- Understand loop iterations and termination conditions
Reading control flow in code:
public void processLights(List<Light> lights, boolean useDefaults) {
for (Light light : lights) { // Loop entry point
if (light.isConnected()) { // Branch point 1
if (useDefaults) { // Branch point 2
light.setBrightness(50);
// This line only executes if both conditions true
} else {
light.setBrightness(getUserPreference());
}
} else {
logDisconnectedDevice(light);
// This line only executes if light is NOT connected
}
}
}
Questions to ask about control flow:
- Which branches are possible given different inputs?
- Are there paths that never execute? (Dead code)
- Are there paths that should execute but don't? (Missing functionality)
- What are the loop termination conditions?
- If there is recursion, what is the base case?
Common control flow bugs:
- Off-by-one errors in loops
- Incorrect boolean logic in conditionals
- Early returns preventing necessary cleanup
- Missing else branches for edge cases
Interprocedural control flow analysis
When examining control flow, you may need to consider not just the current method, but also the methods that are called by the current method. This is called "interprocedural control flow analysis". For example, if you are debugging a method that calls another method, you need to consider the control flow of the called method as well.
IDEs provide tools to help you with this: "Find All References" is a tool that shows you all the places in your codebase that call a given method, whereas "Jump to Definition" is a tool that takes you to the definition of a given method.
Interprocedural control flow is often represented as a "call graph", which is a graph of all the methods in your program and the relationships between them.
Data flow analysis: Tracking how values change
What is data flow? Data flow describes how data moves through your program and how values change. Understanding data flow helps you:
- Track where variables are assigned
- Identify when values are read
- Understand how data transformations occur
- Spot uninitialized variables or unexpected mutations
Reading data flow in code:
public int calculateAverageBrightness(List<Light> lights) {
int total = 0; // Variable defined
int count = 0; // Variable defined
for (Light light : lights) {
if (light.isConnected()) {
total += light.getBrightness(); // total modified
count++; // count modified
}
}
return total / count; // Both variables read - potential divide by zero!
}
Questions to ask about data flow:
- Where is this variable assigned?
- Where is it read?
- What are all possible values it could have at this point?
- Are there unintended mutations?
- Is the variable used before it's properly initialized?
Common data flow bugs:
- Using variables before initialization
- Unintended mutation of shared state
- Divide by zero from unchecked denominators
- Null pointer exceptions from missing null checks
- Off-by-one errors from incorrect index calculations
Combining control flow and data flow analysis
Real bugs often involve both control and data flow issues:
public void activateAreaScene(Area area, String sceneName) {
Scene scene = findScene(sceneName); // Data: scene assigned
if (scene != null) { // Control: branch point
scene.activate();
if (area.hasCascadeEnabled()) { // Control: nested branch
for (Area child : area.getChildren()) { // Control: loop
activateAreaScene(child, sceneName); // Recursive call - both control and data
}
}
}
// Bug: What if scene is null? Silent failure.
// Bug: What if sceneName doesn't exist in child areas? Infinite recursion?
}
Utilize diagrams to visualize control and data flow (5 minutes)
While reading code line-by-line works for simple methods, complex systems require higher-level abstractions. Diagrams help you understand the big picture by visualizing relationships, execution paths, and data transformations.
In this diagram:
- We can see the recursive call (AreaScene.activate calls itself)
- We understand the flow from client request through to device state changes
- The recursion is visually obvious (highlighted in red)
How to create a call graph:
- Hand-sketch first: Don't worry about perfect formatting
- Start with the entry point: The method you're trying to understand
- Add direct calls: Draw arrows to methods it calls
- Expand selectively: Focus on the path you're investigating, not every possible call
- Note recursion: Mark recursive calls clearly
Using AI to generate call graphs:
I'm trying to understand how scene activation works in this codebase.
Here are the relevant methods: [paste methods]
Can you create a Mermaid call graph showing which methods call which
others when activateAreaScene is invoked? Please highlight any recursive
calls.
Of course, you should not trust the output blindly. However, it is far easier to follow the graph step-by-step, convincing yourself that it is correct, than to manually trace the flow of the program. Plus, you get a nicely formatted diagram (that you can easily tweak as needed!).
What sequence diagrams reveal:
- The order of method calls
- Which object is active at each point
- Loops and conditionals in the interaction
- Return values and how they affect flow
- Where bugs might occur in the interaction chain
When to use sequence diagrams:
- Debugging issues involving multiple objects
- Understanding asynchronous operations
- Clarifying callback sequences
- Documenting complex workflows
How to create sequence diagrams:
- Identify the objects involved: Usually 3-6 key objects
- Start with the triggering event: What initiates the sequence?
- Follow the control flow: Each method call becomes an arrow
- Show returns: Dashed arrows show return values
- Mark important data: Add notes for key state changes
Hand-sketching sequence diagrams: You don't need perfect Mermaid syntax while debugging. A quick sketch:
Client → AreaScene.activate()
AreaScene → Scene.activate()
Scene → Device.setState()
AreaScene → Area.getChildren() → [child1, child2]
AreaScene → AreaScene.activate(child1) [RECURSIVE]
...
Describe the scientific method of debugging (15 minutes)
Suggested background reading and source for much of this section: Introduction to Debugging by Andreas Zeller. We directly adapt Andreas' great example (thanks, Andreas for releasing that under CC-BY-NC-SA!)
Just as we've emphasized systematic program design and implementation (Requirements → Design → Implementation → Validation), we'll see how systematic debugging follows a similar process.
The scientific method applied to debugging
Debugging is fundamentally a scientific process of forming and testing hypotheses about program behavior:
- Observe: Notice unexpected behavior or test failure
- Hypothesize: Form a theory about what might be causing the issue
- Predict: Determine what evidence would support or refute the hypothesis
- Test: Gather that evidence (through debugging tools, logging, tests)
- Analyze: Evaluate whether the hypothesis is supported
- Iterate: Refine the hypothesis or move to implementation if confirmed
Example: Debugging a sorting algorithm
- Observe: Test fails - list not properly sorted
- Hypothesize: Comparator logic is incorrect
- Predict: If hypothesis is true, we should see incorrect comparisons in the debugger
- Test: Set breakpoint in compare method, inspect parameters
- Analyze: Compare method returns correct values - hypothesis rejected
- Iterate: Form new hypothesis about loop boundaries
Tighten feedback loops: If you end up needing to reproduce a bug many times in your efforts to fix it, you could end up spending more time reproducing the bug than fixing it. Create a minimal test case that reproduces the bug, and use it to test your fixes. But, do not boil the ocean to create a complex and fully-automated test case.
You may have found that for many bugs, you can skip through this process: you are able to quickly identify a problem and fix it. Unfortunately, we guarantee that you will soon enough encounter a bug that you can not fix in a single session. For these, following this explicit process is essential. As you'll learn in this lecture, AI coding agents can be of tremendous value in helping you follow this process.
A rule of thumb that we suggest is: if you can fix a bug in one or two tries, just do it. If you don't quickly get to the answer, open a new text file to use as a debugging log as you follow the scientific method. The log is immensely useful so that when you sit there and say "but it can't be X because I checked it already", you can just refer to the log to see what exactly you already checked.
Example: Debugging HTML Markup Removal
Let's work through a concrete example using the scientific method. We'll debug a function that removes HTML markup from text, following the systematic hypothesis testing procedure.
The Task: Implement a function that takes an HTML string and returns just the text content, removing all HTML tags.
Initial Implementation:
public static String removeHtmlMarkup(String s) {
boolean tag = false;
boolean quote = false;
StringBuilder out = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '<' && !quote) {
tag = true;
} else if (c == '>' && !quote) {
tag = false;
} else if (c == '"' || c == '\'' && tag) { // BUG: operator precedence issue!
quote = !quote;
} else if (!tag) {
out.append(c);
}
}
return out.toString();
}
Step 1: Observe - Build an observation table
Let's test the function with several test cases and record our observations:
@Test
public void testRemoveHtmlMarkup_removesBasicTags() {
assertEquals("foo", removeHtmlMarkup("<b>foo</b>"));
}
@Test
public void testRemoveHtmlMarkup_preservesQuotesInContent() {
assertEquals("\"foo\"", removeHtmlMarkup("<b>\"foo\"</b>"));
}
@Test
public void testRemoveHtmlMarkup_handlesQuotesBeforeTags() {
assertEquals("\"foo\"", removeHtmlMarkup("\"<b>foo</b>\""));
}
@Test
public void testRemoveHtmlMarkup_handlesAttributes() {
assertEquals("foo", removeHtmlMarkup("<b id=\"bar\">foo</b>"));
}
| Input | Expectation | Output | Outcome |
|---|---|---|---|
<b>foo</b> | foo | foo | ✔ |
<b>"foo"</b> | "foo" | foo | ✘ |
"<b>foo</b>" | "foo" | <b>foo</b> | ✘ |
<b id="bar">foo</b> | foo | foo | ✔ |
Step 2: Hypothesize - Form theories
Based on our observations, we form two hypotheses:
| Hypothesis | Reasoning |
|---|---|
| H1: Double quotes are stripped from the tagged input | We see that <b>"foo"</b> outputs foo instead of "foo" - the double quotes are removed from tagged content |
| H2: Tags in double quotes are not stripped | We see that "<b>foo</b>" outputs <b>foo</b> instead of "foo" - tags aren't removed |
These may be two separate issues, but they're likely related. Let's focus on H1 first because it's simpler.
Step 3: Test Hypothesis 1 - Double quotes are stripped from the tagged input
Let's devise an experiment to validate H1. Does it hold for all inputs, even untagged ones? Our hypothesis becomes:
H1: Double quotes are stripped from the input (refined from "tagged input")
If we feed the string "foo" (including the double quotes) into removeHtmlMarkup(), we should obtain "foo" as result (unchanged). However, if H1 is correct, we should obtain foo (quotes stripped).
@Test
public void testRemoveHtmlMarkup_handlesPlainQuotes() {
assertEquals("\"foo\"", removeHtmlMarkup("\"foo\""));
}
// Test passes
Our hypothesis H1 is confirmed! Double quotes are being stripped from the input, even when there are no HTML tags present. We can add this to our observation table:
| Input | Expectation | Output | Outcome |
|---|---|---|---|
"foo" | "foo" | foo | ✘ |
Where does that quote-stripping come from? The only place in removeHtmlMarkup() where quotes are handled is:
else if (c == '"' || c == '\'' && tag) {
quote = !quote;
}
So, quotes should only toggle the quote flag if tag is set. However, tag can only be set if the input contains a markup tag (like <b>), which is not the case for a simple input like "foo". Hence, what we observe is actually impossible - yet it happens!
Step 5: Refine the hypothesis - Is tag being set?
Debugging is a game of falsifying assumptions. We assume tag cannot be set for "foo" - but maybe it is? Let's create a hypothesis:
H3: The error is due to tag being set.
We can test this using assertions. An assert statement encodes our assumptions - if it fails, something is wrong. Let's add an assertion to check if tag is ever set:
public static String removeHtmlMarkupWithTagAssert(String s) {
boolean tag = false;
boolean quote = false;
StringBuilder out = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
assert !tag; // <=== Just added: tag should never be set for "foo"
if (c == '<' && !quote) {
tag = true;
} else if (c == '>' && !quote) {
tag = false;
} else if (c == '"' || c == '\'' && tag) {
quote = !quote;
} else if (!tag) {
out.append(c);
}
}
return out.toString();
}
We run the test again, and it passes! We can double-check that the assertion works by running the other tests (with tags), which now fail with an assertion error. Hmmm.
Step 6: Refute Hypothesis 3
We did not get an exception, which means tag is never set to true for the input "foo". Hence, we reject H3:
H3: The error is due to tag being set. ❌ REFUTED
Since we know tag is always false, the condition c == '"' || c == '\'' && tag should never hold (because tag is false, so c == '\'' && tag is false, and c == '"' alone shouldn't be enough... or is it?).
Step 7: New hypothesis - The quote condition evaluates to true
Maybe there's something wrong with the condition such that it holds even when tag is false? Here's our new hypothesis:
H4: The error is due to the quote condition evaluating to true (even when tag is false).
Let's insert an assertion that assumes the quote condition is never reached:
public static String removeHtmlMarkupWithQuoteAssert(String s) {
boolean tag = false;
boolean quote = false;
StringBuilder out = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '<' && !quote) {
tag = true;
} else if (c == '>' && !quote) {
tag = false;
} else if (c == '"' || c == '\'' && tag) {
assert false; // <=== Just added: this should never execute
quote = !quote;
} else if (!tag) {
out.append(c);
}
}
return out.toString();
}
// This will throw AssertionError
String result = removeHtmlMarkupWithQuoteAssert("\"foo\"");
Step 8: Confirm Hypothesis 4
The assertion fails, which means the quote condition does evaluate to true even though tag is false! Our hypothesis H4 is CONFIRMED:
H4: The error is due to the quote condition evaluating to true (even when tag is false). ✅ CONFIRMED
The condition c == '"' || c == '\'' && tag is faulty - it evaluates to true even when tag is false. But wait - this condition should hold for both single and double quotes. Is there a difference?
Step 9: Generalize - Test with single quotes
Let's see if our observations generalize. We test with single quotes:
String result = removeHtmlMarkup("'foo'");
System.out.println(result);
// Output: "'foo'" (quotes preserved!)
Surprise! Single quotes are not stripped. Let's add this to our observation table:
| Input | Expectation | Output | Outcome |
|---|---|---|---|
"foo" | "foo" | foo | ✘ (double quotes stripped) |
'foo' | 'foo' | 'foo' | ✔ (single quotes preserved) |
So, the condition:
- becomes
Truewhen a double quote is seen (even whentagis false) - becomes
False(as it should) with single quotes (whentagis false)
Step 10: Understand the root cause
At this point, we have enough information to solve the problem. The issue is operator precedence!
The condition c == '"' || c == '\'' && tag is parsed as:
(c == '"') || (c == '\'' && tag)
Due to operator precedence, && binds tighter than ||, so:
- For double quotes:
c == '"'istrue, so the whole expression istrue(regardless oftag) - For single quotes:
c == '\''istrue, buttagisfalse, soc == '\'' && tagisfalse, making the whole expressionfalse
Step 11: Fix - Implement the solution
The fix is to add parentheses so that quotes only toggle when inside a tag:
public static String removeHtmlMarkup(String s) {
boolean tag = false;
boolean quote = false;
StringBuilder out = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '<' && !quote) {
tag = true;
} else if (c == '>' && !quote) {
tag = false;
} else if ((c == '"' || c == '\'') && tag) { // FIX: Added parentheses!
quote = !quote;
} else if (!tag) {
out.append(c);
}
}
return out.toString();
}
The key fix is changing c == '"' || c == '\'' && tag to (c == '"' || c == '\'') && tag. Now quotes only toggle the quote flag when we're inside a tag, which is the correct behavior.
Step 12: Verify the fix
All our test cases now pass.
Lessons from this example:
- Build observation tables - Systematically record inputs, expectations, outputs, and outcomes
- Test simpler hypotheses first - Start with H1 (double quotes stripped) before tackling H2
- Use assertions to test assumptions -
assert !tagandassert falsehelped us systematically narrow down the bug - Refute hypotheses systematically - H3 was refuted, leading us to H4
- Generalize findings - Testing single quotes revealed the operator precedence issue
- Understand before fixing - We fully understood the bug (operator precedence) before implementing the fix
This example demonstrates how the scientific method helps us systematically identify and fix bugs, even when the root cause is subtle (like operator precedence).
Costs and benefits of systematic debugging
Benefits:
- Maintain artifacts (a debugging log) with hypotheses and evidence, externalizing all of these moving pieces from your short-term memory
- Builds understanding of the codebase
Costs:
- Requires upfront time investment in understanding
- May feel slower initially than trial-and-error
Utilize a debugger to step through a program and inspect state (10 minutes)
Debuggers automate the process of tracing control and data flow, allowing you to observe program execution in real-time. They are powerful tools that support the scientific method of debugging by making it easier to test hypotheses and gather evidence.
How a debugger would have helped with our HTML markup example
Recall our debugging session with removeHtmlMarkup(). We manually traced through execution, added System.out.println statements, and used assertions to test hypotheses. While this worked, a debugger would have made the process much faster and easier.
Instead of manual tracing, we could have:
- Set a breakpoint at the start of the loop in
removeHtmlMarkup() - Run the failing test
testRemoveHtmlMarkup_handlesPlainQuotes()in debug mode - Watch the variables
tagandquoteas we step through execution - Immediately see that when we encounter
"at position 0,quotebecomestrueeven thoughtagisfalse
Instead of adding assertions, we could have:
- Set a conditional breakpoint on the quote-handling line:
c == '"' || c == '\'' && tag - Watch the condition evaluate to
trueeven whentagisfalse - Inspect the expression
c == '"'and see it'strue, confirming our hypothesis about operator precedence
Instead of testing single quotes separately, we could have:
- Run the same test with
'foo'input - Watch the condition evaluate to
false(as expected) - Compare the two executions side-by-side to see the difference
The debugger doesn't replace the scientific method - it accelerates it. You still form hypotheses, but instead of manually tracing code or adding print statements, you can:
- Set breakpoints to test hypotheses about where bugs occur
- Watch variables to test hypotheses about state
- Step through code to understand control flow
- Evaluate expressions to test hypotheses about conditions
Let's see how debuggers work and how to use them effectively.
Core debugger concepts
Breakpoints: Points where execution pauses, allowing you to inspect state
- Line breakpoints: Pause when a specific line is reached
- Conditional breakpoints: Pause only when a condition is true
- Exception breakpoints: Pause when an exception is thrown
Stepping commands:
- Step Over: Execute the current line and pause at the next line
- Step Into: If the current line contains a method call, enter that method
- Step Out: Execute until the current method returns
- Continue: Resume execution until the next breakpoint
Inspection capabilities:
- View variable values at the current point in execution
- Evaluate arbitrary expressions
- View the call stack (how did we get here?)
- Watch variables (track specific values as execution continues)
Effective debugger workflow (applying the scientific method)
The debugger workflow aligns perfectly with the scientific method of debugging we learned earlier:
1. Observe - Reproduce the bug reliably
Before debugging, ensure you can consistently trigger the problem. Write a test case if possible. In our HTML markup example, we had testRemoveHtmlMarkup_handlesPlainQuotes() that reliably failed.
2. Hypothesize - Form a hypothesis about the bug's location Don't just set breakpoints randomly. Use your control and data flow analysis to narrow down where the problem might be. In our example, we hypothesized that the quote condition was evaluating incorrectly.
3. Predict & Test - Set strategic breakpoints
- Start broad: Set a breakpoint at a high-level entry point (like the start of the loop)
- Narrow down: Based on what you observe, move breakpoints closer to the issue
- Use conditional breakpoints to test specific hypotheses (e.g.,
c == '"'to test if double quotes trigger the bug) - Watch variables to test hypotheses about state (e.g., watch
tagandquoteto see when they change)
4. Analyze - Step through execution systematically
- Pay attention to which branches are taken (control flow)
- Watch how variables change (data flow)
- Compare actual values to expected values
- Note when values first become incorrect (this is where the bug manifests)
5. Iterate & Verify - Refine hypotheses based on evidence
- If your hypothesis is confirmed, dig deeper to find the root cause
- If your hypothesis is refuted, form a new hypothesis and test again
- Once you understand the bug, verify by:
- Explaining the bug to yourself or a colleague (rubber duck debugging)
- Confirming the fix in the debugger before changing code
- Writing a test case that would have caught the bug
Example: Using the debugger with our HTML markup bug
- Observe: Run
testRemoveHtmlMarkup_handlesPlainQuotes()- it fails - Hypothesize: The quote condition
c == '"' || c == '\'' && tagevaluates incorrectly - Predict & Test: Set a breakpoint on that line, watch
tagandquotevariables - Analyze: Step through and see
tagisfalsebut condition still evaluates totruefor double quotes - Iterate: Test with single quotes - condition evaluates to
falseas expected - Verify: Understand operator precedence is the root cause, fix it, verify in debugger
Common debugger pitfalls
Over-reliance on stepping:
- Stepping through every line is time-consuming
- Use breakpoints strategically instead
- Use "Continue to Cursor" for long stretches
Not using conditional breakpoints:
- Example: Bug only occurs on iteration 47 of a loop
- Instead of hitting F10 forty-seven times, use a conditional breakpoint:
i == 47
Ignoring the call stack:
- The call stack shows how execution reached this point
- Essential for understanding context and finding the root cause
- Bug might be in the caller, not the current method
Modifying code while debugging without understanding:
- Resist the urge to "fix" code immediately
- First, fully understand the problem
- Then step out of the debugger and make intentional changes
Utilize an AI programming agent to assist with debugging (10 minutes)
AI programming agents can accelerate debugging, but only when used with the systematic approach we've established. This section demonstrates how to integrate AI into the scientific method of debugging.
AI as a debugging assistant: Capabilities and limitations
What AI agents are good at:
- Pattern recognition: Spotting common bug patterns (null checks, off-by-one errors, etc.)
- Boilerplate generation: Creating test cases to reproduce bugs
- Code reading: Quickly analyzing unfamiliar code for potential issues
- Suggesting hypotheses: Proposing multiple theories about what might be wrong
- Explanation: Helping you understand complex code flows
What AI agents struggle with:
- Domain-specific logic: Understanding your business rules and design decisions
- Runtime state: AI can't directly observe your program's execution
- Environmental issues: Configuration problems, system dependencies, race conditions
- Root cause analysis: May suggest fixes for symptoms rather than underlying causes
Critical limitation: AI agents can't execute your code or verify that their suggestions actually fix the problem. You must test and validate everything they suggest.
Applying the 6-step workflow to debugging (from Lecture 13)
Recall the workflow from Lecture 13:
- Identify: What information does the AI need?
- Engage: Craft effective prompts
- Evaluate: Assess AI outputs critically
- Calibrate: Refine based on feedback
- Tweak: Adjust the solution
- Finalize: Document decisions
Applied to debugging:
1. Identify: Gather context before engaging AI Before asking AI for help, collect:
- The unexpected behavior (what went wrong)
- The expected behavior (what should happen)
- Relevant code (not the entire codebase)
- Error messages or stack traces
- Steps to reproduce
- Your current hypothesis (if you have one)
2. Engage: Effective debugging prompts
❌ Ineffective prompt:
This code doesn't work. Fix it.
✅ Effective prompt:
I'm debugging a scene activation feature in my IoT app. When I call
activateAreaScene on a nested area structure, the scene activates on
the top-level area but not on child areas.
Expected: Scene should cascade to all child areas
Actual: Only activates on the parent area
(Tag the relevant method as context)
My hypothesis: The recursive call might not be finding the scene in
child areas. Analyze the code and provide a report that either disproves this hypothesis,
confirms it, or suggests further steps to test it.
Why the second prompt is better:
- Provides specific behavior (not just "doesn't work")
- Includes expected vs actual behavior
- States your hypothesis
- Asks for analysis, not just a fix
3. Evaluate: Critical assessment of AI suggestions
Determine if you are able to evaluate this output critically.
If you are not, you should stop, and change your approach to ask for a more detailed analysis (e.g. more specific hypotheses, or a more detailed analysis of the code). Also consider taking a break from debugging to examine related reference material.
Evaluation checklist:
- ✅ Is the AI's suggested hypothesis plausible given your understanding of the codebase?
- ✅ Is the AI's analysis of the codebase accurate?
- ✅ If there is a fix suggested, is it a valid fix?
4. Calibrate: Iterative refinement
If the AI's suggestion doesn't work or doesn't make sense:
- Provide feedback about what happened when you tested it
- Share additional context (error messages, debugger observations)
- Suggest adding additional logging to help understand the problem
- Ask for alternative hypotheses
5. Tweak: Refine the solution
Even if the AI's suggestion is close, you might need to adjust the output.
6. Finalize: Document the fix
After fixing the bug:
- Write a test case that would have caught it
- Document the root cause in comments or commit messages
- Update design docs if the bug revealed a design issue
When NOT to use AI for debugging
Following principles from Lecture 13, avoid AI for debugging when:
- You don't understand the codebase well enough to evaluate suggestions
- The bug involves system-level issues (configuration, environment, hardware)
- The problem requires domain expertise the AI doesn't have
- You find yourself in a "vibe coding" trap (just trying AI suggestions without understanding)
In these cases, you should still consider using AI for program understanding tasks that are related to debugging, such as analyzing control and data flow in the program. Use these artifacts to increase your understanding of the codebase, and then return to debugging.
The "learning tax" for debugging: Just as we discussed in Lecture 13, deliberately practice debugging manually sometimes, even when AI could help. This builds the expertise that makes you effective when using AI. Even if it is tempting to have AI add logging everywhere and then analyze a huge log file, the practiced skill of intuiting what to capture and what to ignore is a valuable skill that will help you when using AI.