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:
- Stop execution at a precise line of code
- See the values of all variables in scope
- Walk through the execution path step by step
- Change variable values mid-execution to test hypotheses
- Evaluate arbitrary expressions in the current context
- Trace the call stack to understand how you got there
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:
- Debug button — The green bug icon next to the Run button on the toolbar
- Shift+F9 (Windows/Linux) or Ctrl+D (macOS) — Keyboard shortcut to debug the current run configuration
- Right-click on any class with a main method and select "Debug"
- Debug option in the Run menu
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:
- Change values — Right-click a variable and select "Set Value" to modify it during debugging. Use this to test how different inputs affect behavior.
- Copy values — Right-click and copy the value or the full tree as text.
- Mark objects — Use "Mark Object" to assign a label to an object reference so you can track it across different stack frames.
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:
- Step Over (F8) — Execute the current line and move to the next line in the same method. If the line calls a method, that method is executed entirely without stepping into it.
- Step Into (F7) — Enter the method being called on the current line. If multiple methods are called on one line, IntelliJ will let you choose which one to step into.
- Force Step Into (Shift+F7) — Step into a method even if it's marked as "don't step into" via the stepping filters.
- Smart Step Into (Shift+F7) — When a line has multiple method calls, a popup appears letting you select exactly which method to step into.
- Step Out (Shift+F8) — Complete the current method and return to the caller. Useful when you've stepped into a method and realize you don't need to trace it line by line.
- Run to Cursor (Alt+F9) — Resume execution and pause again when the line under the cursor is reached. This is like a temporary, one-shot breakpoint.
- Resume (F9) — Continue execution until the next breakpoint is hit or the program terminates.
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:
- Go to Run → Edit Configurations
- Click + and select "Remote JVM Debug"
- Set the host and port (matching the address in the JVM flags)
- Choose the appropriate JDK version for the remote JVM
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:
- Hot Swap (limited) — The JVM's built-in hot swap capability allows changing method bodies. When you modify code inside a method and recompile (Ctrl+Shift+F9), IntelliJ attempts to hot-swap the new bytecode into the running JVM.
- DCEVM (Dynamic Code Evolution VM) — An enhanced JVM that supports adding/removing methods and fields, not just changing method bodies. Install DCEVM as your JDK to get much more powerful hot-swap capabilities.
- JRebel integration — IntelliJ integrates with JRebel, a commercial plugin that provides near-instant code reloading for framework-based applications.
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:
- Spring Boot — Run/debug configurations automatically detect Spring Boot applications. Breakpoints in controllers, services, and repositories work as expected.
- Jakarta EE / Tomcat — IntelliJ can deploy and debug directly to Tomcat servers, with breakpoints triggering on HTTP requests.
- JavaScript/TypeScript — The built-in JavaScript debugger works with both client-side code (via Chrome/Firefox integration) and Node.js backends.
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:
- Is the breakpoint even being hit? If not, the code path isn't executing as expected.
- Are the inputs what you expect? Inspect method arguments and external data.
- Is the state correct at each step? Walk through the method verifying each transformation.
- Are side effects occurring? Check fields, collections, and external resources after method calls.
- Is the return value correct? Step out and inspect what's returned to the caller.
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:
- F8 — Step Over
- F7 — Step Into
- Shift+F8 — Step Out
- Alt+F9 — Run to Cursor
- Alt+F8 — Evaluate Expression
- Ctrl+Shift+F8 — View/Edit All Breakpoints
- F9 — Resume
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.