Debugging Java Programs — Techniques, Tools & Best Practices
Every programmer encounters bugs. The difference between a junior and senior developer isn’t the number of bugs they write — it’s how quickly they find and fix them.
Types of Errors
| Type | When It Happens | Example |
|---|---|---|
| Compile-time | During compilation | Missing semicolon, wrong type |
| Runtime | During execution | NullPointerException, division by zero |
| Logical | Program runs but gives wrong results | Off-by-one error, wrong formula |
Compile-time errors are the easiest — the compiler tells you exactly what’s wrong. Logical errors are the hardest — the program doesn’t crash, it just gives wrong answers.
Reading Error Messages
Java error messages are incredibly helpful if you read them carefully.
Compile Error
Main.java:5: error: incompatible types: String cannot be converted to int
int x = "hello";
^
Tells you: file, line number, what’s wrong.
Runtime Exception (Stack Trace)
Exception in thread "main" java.lang.NullPointerException:
Cannot invoke "String.length()" because "name" is null
at Main.processName(Main.java:15)
at Main.main(Main.java:8)
Read bottom to top:
maincalledprocessName(line 8)processNamecrashed at line 15- Reason: tried to call
.length()on anullString
Always read the first line of the stack trace — it tells you what went wrong. Then trace the call chain.
Debugging with Print Statements
The simplest technique — add System.out.println() to see variable values:
public static int binarySearch(int[] arr, int target) {
int low = 0, high = arr.length - 1;
while (low <= high) {
int mid = (low + high) / 2;
System.out.println("low=" + low + " high=" + high + " mid=" + mid + " arr[mid]=" + arr[mid]);
if (arr[mid] == target) return mid;
if (arr[mid] < target) low = mid + 1;
else high = mid - 1;
}
return -1;
}
Pros and Cons
| ✅ Pros | ❌ Cons |
|---|---|
| Quick and easy | Clutters code |
| Works anywhere | Must remove after debugging |
| No tools needed | Can’t inspect complex objects easily |
Tip: Use a consistent prefix like
[DEBUG]so you can find and remove them easily.
Using an IDE Debugger
IDE debuggers (IntelliJ IDEA, VS Code, Eclipse) are far more powerful than print statements.
Key Concepts
| Feature | What It Does |
|---|---|
| Breakpoint | Pauses execution at a specific line |
| Step Over (F10) | Execute current line, move to next |
| Step Into (F11) | Enter a method call |
| Step Out (Shift+F11) | Finish current method, return to caller |
| Continue (F5) | Run until next breakpoint |
| Watch | Monitor a variable’s value |
| Evaluate | Run expressions while paused |
How to Debug in an IDE
- Set a breakpoint — click the line number gutter
- Run in debug mode — click Debug (not Run)
- Inspect variables — hover over them or check the Variables panel
- Step through — use Step Over/Into to follow execution
- Evaluate expressions — test values in the Debug Console
Conditional Breakpoints
Only pause when a condition is met:
Breakpoint at line 15: i == 50
This is invaluable when debugging loops — you don’t want to step through 50 iterations.
Common Bugs and How to Fix Them
NullPointerException
String name = null;
System.out.println(name.length()); // NPE!
Fix: Check for null before using:
if (name != null) {
System.out.println(name.length());
}
ArrayIndexOutOfBoundsException
int[] arr = {1, 2, 3};
System.out.println(arr[3]); // index 3 doesn't exist!
Fix: Check bounds: if (index >= 0 && index < arr.length)
Off-By-One Errors
// Bug: skips the last element
for (int i = 0; i < arr.length - 1; i++) { }
// Fix
for (int i = 0; i < arr.length; i++) { }
Infinite Loops
int i = 0;
while (i < 10) {
System.out.println(i);
// forgot i++!
}
Fix: Ensure the loop variable is updated.
Integer Division
double avg = 7 / 2; // 3.0, not 3.5!
// Fix
double avg = 7.0 / 2; // 3.5
String Comparison with ==
if (input == "yes") { } // wrong!
if (input.equals("yes")) { } // correct
Systematic Debugging Strategy
When facing a bug, follow this process:
- Reproduce — Can you make the bug happen consistently?
- Isolate — What’s the smallest input that triggers it?
- Hypothesize — What do you think is wrong?
- Test — Add a breakpoint or print to verify
- Fix — Change the code
- Verify — Does the fix work? Did it break anything else?
Logging vs Print Statements
For real applications, use a logging framework instead of System.out.println():
import java.util.logging.Logger;
Logger logger = Logger.getLogger("MyApp");
logger.info("Processing order #" + orderId);
logger.warning("Inventory low for item: " + itemId);
logger.severe("Database connection failed!");
Benefits:
- Log levels (INFO, WARNING, SEVERE)
- Can write to files
- Can be enabled/disabled without code changes
- Timestamps and context included automatically
Quick Exercises
- This code has a bug. Find and fix it:
int[] nums = {1, 2, 3, 4, 5}; int sum = 0; for (int i = 1; i <= nums.length; i++) { sum += nums[i]; } - Set a breakpoint and step through a recursive Fibonacci method for
n = 5. - Write a method with an intentional logical error and use print statements to find it.
Summary
| Technique | Best For |
|---|---|
| Read error messages | Compile errors, exceptions |
| Print statements | Quick checks |
| IDE debugger | Complex logic, stepping through code |
| Conditional breakpoints | Bugs in loops |
| Logging | Production applications |
Debugging mindset:
- Read the error message carefully
- Don’t guess — observe actual values
- Simplify the problem — use minimal test cases
- Take breaks — fresh eyes find bugs faster
🎉 Module Complete!
Congratulations! You’ve completed Core Java Basics. You now know:
- Variables, data types, and type casting
- Operators and expressions
- Input/output
- Control flow (if-else, switch, loops)
- Methods, scope, and recursion
- Clean code and debugging
You’re ready for the next module — Object-Oriented Programming: where Java truly shines.