How to Debug Java Code

Every programmer spends time debugging. Some estimates suggest developers spend 35-50% of their time finding and fixing bugs. Getting better at debugging directly translates to getting more done.

This guide covers the debugging techniques that work. We’ll start with the basics and move into IDE debuggers, which are more powerful than most beginners realize.

Print Debugging: The Starting Point

The simplest debugging technique is adding print statements to see what your code is actually doing. It’s not sophisticated, but it works.

public int calculateTotal(List<Integer> prices) {
    System.out.println("calculateTotal called with: " + prices);
    
    int total = 0;
    for (int price : prices) {
        System.out.println("Adding price: " + price + ", running total: " + total);
        total += price;
    }
    
    System.out.println("Final total: " + total);
    return total;
}

Print debugging has real advantages. It requires no setup. It works everywhere. You can commit the output to a log file for later analysis.

But it also has limits. Adding and removing print statements gets tedious. You can’t pause execution to inspect state. And you have to recompile after every change. For anything beyond quick checks, use a real debugger.

Reading Stack Traces

When your program crashes, Java prints a stack trace. Learning to read these saves enormous time.

Exception in thread "main" java.lang.NullPointerException
    at com.example.UserService.getUsername(UserService.java:42)
    at com.example.ProfileController.displayProfile(ProfileController.java:28)
    at com.example.Main.main(Main.java:15)

Read from top to bottom. The first line tells you what went wrong: a NullPointerException. The lines below show the call stack, with the most recent call at the top.

The actual bug is at UserService.java, line 42. That’s where the null reference was accessed. But the root cause might be earlier. Something passed null to getUsername(), and you need to trace back to find where that null originated.

Look for your own code in the stack trace. Framework classes (lines mentioning Spring, Hibernate, or java.util) are rarely where your bug lives. Scan for your package names.

Using the IDE Debugger

IntelliJ IDEA, Eclipse, and VS Code all include powerful debuggers. The interface differs slightly, but the concepts are identical.

Setting Breakpoints

A breakpoint pauses execution at a specific line. Click in the gutter next to a line number (or press Ctrl+F8 in IntelliJ, F9 in Eclipse) to set one.

When execution hits the breakpoint, the program freezes. You can inspect variables, step through code line by line, and examine the call stack.

public Order processOrder(Cart cart) {
    double subtotal = cart.getSubtotal();      // Set breakpoint here
    double tax = calculateTax(subtotal);
    double shipping = calculateShipping(cart);
    
    return new Order(subtotal, tax, shipping);
}

With a breakpoint on the first line, you can verify that cart contains what you expect before any processing happens.

Stepping Through Code

Once paused at a breakpoint, you have several options:

Step Over (F8 in IntelliJ, F6 in Eclipse) executes the current line and moves to the next. If the line contains a method call, it runs the entire method without showing you the internals.

Step Into (F7) enters method calls. If the current line is double tax = calculateTax(subtotal), Step Into takes you inside calculateTax() to see what happens there.

Step Out (Shift+F8) finishes the current method and returns to the caller. Useful when you’ve stepped into a method and seen enough.

Resume (F9) continues normal execution until the next breakpoint or the program ends.

Inspecting Variables

While paused, hover over any variable to see its current value. The debugger shows primitives directly and lets you expand objects to see their fields.

The Variables panel (usually in the bottom-left during debugging) shows all variables in the current scope. You can also add variables to a Watch list to track them across multiple breakpoints.

public void processUsers(List<User> users) {
    for (User user : users) {
        // Pause here and inspect 'user' object
        // Expand to see user.name, user.email, user.accountStatus
        sendNotification(user);
    }
}

Conditional Breakpoints

Regular breakpoints pause every time execution reaches them. In a loop processing 10,000 items, that’s not helpful.

Conditional breakpoints only trigger when a condition is true. Right-click a breakpoint and add a condition:

// Condition: user.getId() == 5023
// Or: user.getEmail().contains("problem")
// Or: index > 9990

for (User user : users) {
    processUser(user);  // Breakpoint with condition
}

Now execution only pauses when processing user 5023, or whatever condition you specified. This is invaluable for debugging issues that only appear with specific data.

Evaluate Expression

While paused, you can run arbitrary Java expressions against the current program state. In IntelliJ, press Alt+F8. In Eclipse, use Ctrl+Shift+I.

This lets you test theories without modifying code:

// While debugging, evaluate expressions like:
users.size()
users.stream().filter(u -> u.isActive()).count()
cart.getItems().get(0).getPrice() * 1.08
user.getAddress() != null ? user.getAddress().getCity() : "No address"

You can even call methods and modify variables during debugging, though be careful since this changes program state.

Common Bug Patterns

Certain bugs appear repeatedly in Java code. Recognizing these patterns speeds up debugging.

NullPointerException

The most common Java exception. Something you expected to have a value is null.

// Bug: user might be null
String email = user.getEmail();  // NullPointerException if user is null

// Fix: check for null first
if (user != null) {
    String email = user.getEmail();
}

// Or use Optional (Java 8+)
String email = Optional.ofNullable(user)
    .map(User::getEmail)
    .orElse("default@example.com");

When you see a NullPointerException, ask: where did this null come from? Trace backward through the code to find the source.

Off-By-One Errors

Arrays are zero-indexed. Loops often iterate one too many or one too few times.

// Bug: ArrayIndexOutOfBoundsException
String[] names = {"Alice", "Bob", "Charlie"};
for (int i = 0; i <= names.length; i++) {  // Should be < not <=
    System.out.println(names[i]);
}

// Fix
for (int i = 0; i < names.length; i++) {
    System.out.println(names[i]);
}

// Or use enhanced for loop to avoid index management entirely
for (String name : names) {
    System.out.println(name);
}

Comparing Objects with ==

The == operator compares references, not values. Two strings with identical content are still different objects.

String input = scanner.nextLine();
String expected = "yes";

// Bug: compares references, often returns false
if (input == expected) {
    // This might not execute even when user types "yes"
}

// Fix: use equals() for content comparison
if (input.equals(expected)) {
    // Works correctly
}

// Even safer: put literal first to avoid NullPointerException
if ("yes".equals(input)) {
    // Works even if input is null
}

Integer Division Truncation

Dividing two integers produces an integer, truncating any decimal portion.

// Bug: result is 0, not 0.5
int completed = 1;
int total = 2;
double progress = completed / total;  // 1/2 = 0 in integer math

// Fix: cast to double before division
double progress = (double) completed / total;  // 0.5

Modifying Collections During Iteration

Removing items from a list while looping through it throws ConcurrentModificationException.

// Bug: ConcurrentModificationException
for (String item : items) {
    if (item.startsWith("temp")) {
        items.remove(item);
    }
}

// Fix: use Iterator's remove method
Iterator<String> iterator = items.iterator();
while (iterator.hasNext()) {
    if (iterator.next().startsWith("temp")) {
        iterator.remove();
    }
}

// Or use removeIf (Java 8+)
items.removeIf(item -> item.startsWith("temp"));

Debugging Strategies

Tools matter, but strategy matters more. Here’s how experienced developers approach debugging.

Reproduce the bug first. If you can’t make it happen consistently, you can’t verify it’s fixed. Write down the exact steps that trigger the problem.

Read the error message. This sounds obvious, but many developers glance at “NullPointerException” and start guessing. The stack trace tells you exactly where to look. Read it carefully.

Check recent changes. If the code worked yesterday, what changed? Version control makes this easy. Use git diff or your IDE’s history to see what’s different.

Isolate the problem. Comment out code until the bug disappears, then add it back piece by piece. Or create a minimal test case that reproduces the issue. Smaller code is easier to debug.

Rubber duck debugging. Explain the problem out loud, line by line. Describe what each line should do. Often you’ll spot the mistake while explaining. The “duck” can be a colleague, a stuffed animal, or empty air.

Take breaks. Staring at the same code for hours rarely helps. Walk away. Work on something else. The solution often appears when you return with fresh eyes.

Logging for Production

Print statements don’t belong in production code. Use a logging framework instead.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OrderService {
    private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
    
    public Order processOrder(Cart cart) {
        logger.debug("Processing order for cart: {}", cart.getId());
        
        try {
            Order order = createOrder(cart);
            logger.info("Order created successfully: {}", order.getId());
            return order;
        } catch (Exception e) {
            logger.error("Failed to process order for cart: {}", cart.getId(), e);
            throw e;
        }
    }
}

Logging frameworks like SLF4J with Logback let you control verbosity through configuration. Set DEBUG level during development, INFO or WARN in production. No code changes required.

Log meaningful information: what operation started, what data it received, whether it succeeded or failed. Future-you debugging a production issue will be grateful.


Related: Best Java IDEs Compared | Java Exception Handling | Your First Java Program

Sources

  • JetBrains. “Debugging in IntelliJ IDEA.” jetbrains.com/help/idea/debugging-code.html
  • Eclipse Foundation. “Eclipse Debugger.” eclipse.org/documentation
  • Oracle. “Java Logging Overview.” docs.oracle.com
Scroll to Top