← Back to DevBytes

IntelliJ IDEA Debugging: Complete Guide

Introduction to Debugging in IntelliJ IDEA

Debugging is the systematic process of identifying, isolating, and resolving defects in software. IntelliJ IDEA provides one of the most powerful and intuitive debugging environments available to Java developers. Unlike simple print-statement debugging, the IntelliJ debugger allows you to pause execution at any point, inspect the entire application state, modify variables on the fly, and step through code line by line — all without modifying a single line of source code.

The debugger is deeply integrated into the IDE, supporting not just Java but also Kotlin, Groovy, Scala, JavaScript, TypeScript, and many other languages through plugins. It works with local applications, remote servers, and even inside Docker containers.

What Makes a Debugger Different from Logging?

Logging tells you what happened after the fact. A debugger shows you what is happening right now. With a debugger, you can:

Setting Up Your First Debug Session

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Before diving into advanced features, let's start with the fundamentals. IntelliJ IDEA offers multiple ways to launch a debug session:

Consider this simple Java application that we'll use throughout the guide:

public class OrderProcessor {

    public static void main(String[] args) {
        OrderProcessor processor = new OrderProcessor();
        List orders = processor.fetchPendingOrders();
        
        for (Order order : orders) {
            double total = processor.calculateTotal(order);
            if (total > 100.0) {
                processor.applyDiscount(order, 0.10);
            }
            processor.shipOrder(order);
        }
    }

    private List fetchPendingOrders() {
        // Simulates fetching from database
        return Arrays.asList(
            new Order("ORD-001", "Alice", Arrays.asList(
                new OrderItem("Widget", 25.0, 2),
                new OrderItem("Gadget", 45.0, 1)
            )),
            new Order("ORD-002", "Bob", Arrays.asList(
                new OrderItem("Thingamajig", 200.0, 1)
            )),
            new Order("ORD-003", "Charlie", Arrays.asList(
                new OrderItem("Doohickey", 10.0, 3)
            ))
        );
    }

    public double calculateTotal(Order order) {
        double sum = 0.0;
        for (OrderItem item : order.getItems()) {
            sum += item.getPrice() * item.getQuantity();
        }
        return sum;
    }

    public void applyDiscount(Order order, double discountRate) {
        double originalTotal = calculateTotal(order);
        double discountedTotal = originalTotal * (1 - discountRate);
        order.setDiscountApplied(true);
        order.setDiscountAmount(originalTotal - discountedTotal);
        System.out.println("Discount applied to order " + order.getId());
    }

    public void shipOrder(Order order) {
        // Simulates shipping logic
        if (order.isDiscountApplied()) {
            System.out.println("Shipping order " + order.getId() + 
                               " with discount: $" + order.getDiscountAmount());
        } else {
            System.out.println("Shipping order " + order.getId() + 
                               " at full price");
        }
    }
}

class Order {
    private String id;
    private String customerName;
    private List items;
    private boolean discountApplied;
    private double discountAmount;

    public Order(String id, String customerName, List items) {
        this.id = id;
        this.customerName = customerName;
        this.items = items;
        this.discountApplied = false;
        this.discountAmount = 0.0;
    }

    // Getters and setters omitted for brevity
    public String getId() { return id; }
    public String getCustomerName() { return customerName; }
    public List getItems() { return items; }
    public boolean isDiscountApplied() { return discountApplied; }
    public void setDiscountApplied(boolean discountApplied) { 
        this.discountApplied = discountApplied; 
    }
    public double getDiscountAmount() { return discountAmount; }
    public void setDiscountAmount(double discountAmount) { 
        this.discountAmount = discountAmount; 
    }
}

class OrderItem {
    private String name;
    private double price;
    private int quantity;

    public OrderItem(String name, double price, int quantity) {
        this.name = name;
        this.price = price;
        this.quantity = quantity;
    }

    public String getName() { return name; }
    public double getPrice() { return price; }
    public int getQuantity() { return quantity; }
}

Breakpoints: The Heart of Debugging

Breakpoints are markers that tell the debugger to pause execution when a specific line is reached. IntelliJ IDEA supports several types of breakpoints, each serving a different purpose.

Line Breakpoints

The most common type. Click in the gutter (the area to the left of the line numbers) to set a line breakpoint. A red circle appears, indicating the breakpoint is active. Click again to toggle it off, or right-click to configure its properties.

Let's set a line breakpoint inside the calculateTotal method to inspect how the sum accumulates:

public double calculateTotal(Order order) {
    double sum = 0.0;
    for (OrderItem item : order.getItems()) {
        sum += item.getPrice() * item.getQuantity(); // SET BREAKPOINT HERE
    }
    return sum;
}

When execution hits this line, the debugger pauses. You can then inspect sum, item.getPrice(), and item.getQuantity() in the Variables panel.

Conditional Breakpoints

Sometimes you only want to pause when a specific condition is true. Right-click on a breakpoint and enter a condition. The breakpoint icon changes to include a question mark, visually distinguishing it from unconditional breakpoints.

For example, to pause only when processing orders with an ID of "ORD-002":

// Right-click the breakpoint on this line and set condition:
// order.getId().equals("ORD-002")

for (Order order : orders) {
    double total = processor.calculateTotal(order); // Conditional breakpoint here
    ...
}

The condition is written in the language of the code being debugged and has access to all variables in scope. You can use complex boolean expressions, method calls, and even static helpers.

Method Breakpoints

Set a breakpoint on a method declaration to pause when the method is entered or exited. The breakpoint appears as a diamond icon on the method signature. This is particularly useful for tracking calls to library methods or when you want to see the return value without stepping through the entire method body.

// Set a method breakpoint on this signature
public void applyDiscount(Order order, double discountRate) {
    ...
}

By default, method breakpoints pause on entry. In the breakpoint properties dialog, you can also enable "Emulate after method exit" to pause after the method returns and inspect the state.

Exception Breakpoints

Exception breakpoints pause execution whenever an exception is thrown, even if it's later caught. This is invaluable for tracking down swallowed exceptions. Go to Run → View Breakpoints (Ctrl+Shift+F8), then under "Java Exception Breakpoints" click the + icon and choose an exception type like NullPointerException.

You can configure exception breakpoints to pause on caught exceptions, uncaught exceptions, or both. You can also add conditions to filter by exception message or stack trace content.

Field Watchpoints

Set a breakpoint on a field declaration to pause whenever that field is read or written. The breakpoint appears as a red circle on the field line. This is extremely helpful for tracking down unexpected state changes.

class Order {
    private boolean discountApplied; // Field watchpoint here
    ...
}

In the watchpoint properties, you can choose to pause on field access, field modification, or both.

The Debug Tool Window and Execution Control

When a breakpoint is hit, IntelliJ IDEA displays the Debug tool window. This is your command center during a debug session. The window is divided into several key areas:

Frames / Call Stack

The Frames panel shows the current call stack. Each frame represents a method invocation. Click any frame to jump to that method and see its local variables. This is how you trace back through the execution path to understand how you arrived at the current point.

Variables

The Variables panel displays all variables in the current scope. You can expand object references to drill into nested fields. Primitive values are shown directly. You can also:

Watches

Watches are expressions you want to monitor continuously, even if they're not local variables in the current frame. Click the + icon in the Watches panel and enter any expression valid in the current context.

// Example watch expressions:
order.getItems().size()
order.getCustomerName().toUpperCase()
items.stream().filter(i -> i.getPrice() > 50).count()

Watches re-evaluate at every step, giving you live feedback as the program state evolves.

Stepping Commands

IntelliJ IDEA provides several stepping actions to control execution granularity:

Drop Frame

One of the most powerful debugging features: you can literally rewind execution. In the Frames panel, right-click a frame and select "Drop Frame." This pops the selected frame and all frames above it off the stack, returning execution to just before that method was called. You can then step back into it with different data or after changing variables. This is not true time-travel debugging — it manipulates the call stack — but it's incredibly useful for replaying a method with modified state.

Evaluating Expressions on the Fly

The Evaluate Expression feature (Alt+F8) opens a dialog where you can execute arbitrary code in the current debugging context. This is essentially a REPL that has access to all variables, fields, and methods in scope at the breakpoint.

Examples of what you can evaluate:

// Check a complex condition
order.getItems().stream().anyMatch(item -> item.getPrice() > 100)

// Call a method with test parameters
processor.calculateTotal(new Order("TEST", "TestUser", Collections.emptyList()))

// Modify state (side effects are real!)
order.setDiscountApplied(false)

// Execute multi-line code blocks
double testSum = 0.0;
for (OrderItem item : order.getItems()) {
    testSum += item.getQuantity() * item.getPrice();
}
System.out.println("Recalculated total: " + testSum);
return testSum;

The evaluation dialog also supports code completion, syntax highlighting, and even lambda expressions. Any side effects from evaluated code actually modify the running program's state, so use this power carefully.

Advanced Breakpoint Features

Breakpoint Triggers and Dependencies

IntelliJ IDEA allows you to create chains of breakpoints. A breakpoint can be configured to activate only after another breakpoint has been hit. Right-click a breakpoint, select "More," and under "Catch" or "Thread" policies, you can set dependencies.

For example, you might have a breakpoint deep in a loop that you only want active after a specific entry condition is met at an earlier breakpoint. This avoids having to manually resume hundreds of times.

Logging Breakpoints

Sometimes you don't want to pause at all — you just want to log information to the console without modifying source code. In the breakpoint properties, enable "Evaluate and log" and enter an expression. The expression result is printed to the console each time the breakpoint is hit, but execution continues uninterrupted.

// Configure a logging breakpoint on this line with the expression:
// "Processing order: " + order.getId() + " for customer: " + order.getCustomerName()

for (Order order : orders) {
    double total = processor.calculateTotal(order); // Logging breakpoint here
    ...
}

This is like adding temporary print statements without touching the codebase — perfect for understanding flow in production-like code you can't easily modify.

Remove Once Hit

For breakpoints that only need to trigger once, enable "Remove once hit" in the breakpoint properties. The breakpoint automatically deletes itself after the first hit, keeping your debugging session clean.

Stream and Lambda Debugging

Modern Java code relies heavily on streams and lambdas. Debugging these constructs requires special attention. IntelliJ IDEA provides a dedicated "Stream Trace" feature.

When paused inside a stream pipeline, look for the "Trace Current Stream Chain" button in the Debug tool window. It opens a visualization showing each intermediate operation and the elements flowing through them.

Consider this stream-heavy code:

public List findHighValueCustomers(List orders) {
    return orders.stream()
        .filter(order -> order.getItems().stream()
            .mapToDouble(item -> item.getPrice() * item.getQuantity())
            .sum() > 100)
        .map(Order::getCustomerName)
        .distinct()
        .sorted()
        .collect(Collectors.toList());
}

Set a breakpoint on the collect line and use Stream Trace to see exactly which orders passed the filter, how the mapping transformed them, and what the final collection looks like — all without writing debug code.

Debugging Multi-Threaded Applications

Debugging concurrent code is notoriously difficult. IntelliJ IDEA provides thread-level control to help.

In the Frames panel, the thread selector at the top shows all active threads. When a breakpoint is hit, you can see which thread hit it. By default, a breakpoint suspends only the thread that hit it (not the entire JVM), allowing other threads to continue running.

You can change this in breakpoint properties to "Suspend: All" if you need to freeze the entire application. The Threads view (available via the "Threads" tab or by clicking the thread icon) shows a visual representation of all threads and their states.

// Debugging a race condition scenario
public class InventoryManager {
    private int stock = 100;

    public void purchase(int quantity) {
        if (stock >= quantity) {
            // Set a breakpoint here with suspend policy "Thread"
            // to observe race conditions
            try { Thread.sleep(10); } catch (Exception e) {}
            stock -= quantity;
        }
    }
}

For hard-to-reproduce concurrency bugs, set a breakpoint inside the critical section and use the "Thread" suspend policy. Then run multiple threads simultaneously — the breakpoint will catch each thread as it enters, and you can inspect their interleaving.

Remote Debugging

IntelliJ IDEA can debug applications running on remote machines, in containers, or on production servers (with appropriate caution). This works via the Java Debug Wire Protocol (JDWP).

To enable remote debugging on the target JVM, start it with these flags:

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -jar myapp.jar

Then in IntelliJ IDEA, create a Remote JVM Debug run configuration:

When you run this configuration, IntelliJ connects to the remote JVM. You can set breakpoints in your local source code (which must match the remote application's version), and they'll trigger when the remote code executes. The Variables, Watches, and Evaluate Expression features all work as if you were debugging locally.

For Docker containers, IntelliJ can automatically configure remote debugging when you use the Docker run configuration with the debug mode enabled.

Hot-Swap and Reload Features

IntelliJ IDEA supports several approaches to updating code without restarting the debug session:

When hot-swap fails (indicated by a notification), you'll need to restart the debug session. IntelliJ makes this fast with automatic build-on-restart.

Debugging Tests

You can debug unit tests exactly like application code. Right-click a test method or class and select "Debug" instead of "Run." The debugger pauses at breakpoints inside the test, the production code under test, and even in setup/teardown methods.

This is particularly valuable for investigating test failures that only occur in CI environments. By debugging the test locally, you can inspect the exact state that leads to the failure.

@Test
public void testDiscountApplication() {
    OrderProcessor processor = new OrderProcessor();
    Order order = new Order("ORD-TEST", "TestCustomer", 
        Arrays.asList(new OrderItem("ExpensiveItem", 150.0, 1)));
    
    double total = processor.calculateTotal(order);
    // Set a breakpoint here and step into applyDiscount
    if (total > 100.0) {
        processor.applyDiscount(order, 0.10);
    }
    
    assertTrue(order.isDiscountApplied());
    assertEquals(135.0, total, 0.01); // Bug: total is still 150.0
}

Debugging this test reveals that total is a local variable that isn't updated by applyDiscount — a logic error that's immediately obvious when you step through the code.

Debugging Web Applications and Frameworks

IntelliJ IDEA's debugger integrates seamlessly with web frameworks:

For Spring Boot, you can set breakpoints in configuration classes to debug bean creation, in AOP aspects to trace advice application, and in security filters to understand authentication flows.

Best Practices for Effective Debugging

1. Start with a Hypothesis

Before launching the debugger, form a specific hypothesis about what might be wrong. "The discount isn't being applied" is too vague. "The discount is calculated but the order object isn't being updated with the discounted total" is a testable hypothesis you can verify with breakpoints and variable inspection.

2. Use Conditional Breakpoints Sparingly

While powerful, conditional breakpoints evaluate the condition every time the line is reached, which can significantly slow down execution in hot loops. For frequently-hit lines, consider using a logging breakpoint instead to collect data, then set a regular breakpoint once you've narrowed down the problematic case.

3. Leverage Evaluate Expression Liberally

The Evaluate Expression dialog is your laboratory during debugging. Use it to test potential fixes by running corrected code and observing the result, all without leaving the debug session. You can call methods with different arguments, construct alternative objects, and validate your mental model of the system.

4. Mark Objects for Tracking

When debugging complex object graphs, use the "Mark Object" feature (right-click a variable → Mark Object) to assign a label like "originalOrder" or "afterDiscount." The label appears wherever that object reference is displayed, helping you track the same instance across different stack frames and method calls.

5. Create a Debugging Checklist

When facing a persistent bug, systematically work through:

6. Use Multiple Breakpoint Types Together

Combine exception breakpoints with conditional line breakpoints. For example, set an exception breakpoint on IllegalStateException and a conditional line breakpoint near where you suspect the illegal state originates. This triangulates the root cause from both directions.

7. Keep Debugging Sessions Short

Long debugging sessions often indicate you're fixing symptoms rather than root causes. If you've been debugging the same issue for more than 30 minutes, step back and reconsider your approach. Write a minimal reproduction test, add targeted logging, or pair-debug with a colleague.

8. Document Breakpoint Sets

IntelliJ IDEA allows you to save and export breakpoints. For complex debugging scenarios, save your breakpoint configuration (Run → View Breakpoints → Export) so you can share it with team members or restore it later. This is especially valuable for onboarding new developers to complex codebases.

9. Master Keyboard Shortcuts

The difference between fluid debugging and frustrating debugging often comes down to keyboard proficiency. Essential shortcuts to internalize:

10. Never Debug on Production

Remote debugging to production servers is dangerous. A breakpoint set on a high-traffic endpoint can freeze all request threads, effectively causing a denial of service. If you must debug in production-like environments, use logging breakpoints (which don't suspend), or take a thread dump and analyze it offline. For critical issues, reproduce them in a staging environment with remote debugging enabled.

Conclusion

The IntelliJ IDEA debugger is a remarkably sophisticated tool that transforms bug investigation from guesswork into a precise, methodical process. From basic line breakpoints to conditional triggers, from expression evaluation to stream tracing, from local debugging to remote JVM connections — the debugger meets developers at every skill level and scales with the complexity of the problem.

The key to mastering debugging is not memorizing every feature, but developing a systematic approach: form a hypothesis, set strategic breakpoints, inspect state, evaluate potential fixes, and iterate. Each feature described in this guide — conditional breakpoints, evaluate expression, drop frame, marked objects, stream trace — serves that investigative workflow.

Invest time in learning these tools. The hours you spend becoming fluent with the debugger will repay themselves many times over in faster bug resolution, deeper code understanding, and the quiet confidence that comes from knowing you can crack even the most stubborn defects.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles