5 minutes read

POSTED Mar, 2021 dot IN Debugging

Debugging Tips for Developers on IntelliJ IDEA

Serkan Özal

Written by Serkan Özal

Founder and CTO of Thundra


9 Tips to Supercharge Your IntelliJ IDEA Debugging Skills

Developers are often discouraged by the amount of time spent debugging code. Time spent debugging is time away from developing interesting new features. So how can this pain point be alleviated?

The first way to cut down on time spent debugging is to write better code. Code without any of those tricky, off-by-one errors and null object dereferences. Ideal code.

But we all know it’s impossible to write perfect code on the first try. Besides, requirements change. What was perfectly functional code a week ago may be broken now due to a change in another part of a program.

The second, and more realistic method to save time is to improve your debugging skills and use better debugging tools more efficiently. Not only does this reduce the time and pain required to debug a program, but it expands the limits of the debugging itself, allowing you to debug remote, and even serverless, applications.

In this post we’ll unveil some lesser-known IntelliJ IDEA features that will supercharge your debugging skills.

Breakpoint Types

Let’s start with the basics: breakpoint types. Developers are used to regular line breakpoints, suspending a program upon reaching a specific line of code. But other types of breakpoints exist:

  • Method breakpoints: These suspend a program upon entering or exiting a specific method or its implementation. To place it in your code, click in the gutter area as you would for a regular breakpoint, but on a line with a method declaration itself rather than inside its code.
  • Field watchpoints: When a specified field is accessed, field watchpoints will suspend a program. Think of it as a pair of method breakpoints, one for getter and another for setter—except field watchpoints work for fields without getters and setters. To place a field watchpoint breakpoint, click in the gutter area opposite to that field.
  • Exception breakpoints: These breakpoints will suspend a program when an exception occurs. Exception breakpoints are set globally via breakpoints view (Run → View Breakpoints), and are triggered by a specific exception class. They also have extra knobs to configure, such as filtering out the classes where an exception occured or triggering specifically for uncaught or caught exceptions.
  • Non-suspending breakpoints (or tracepoints): Why use a breakpoint that doesn’t suspend a program at all? Because they allow us to trace executions and log information in process. To make a regular breakpoint non-suspending, open its properties and uncheck the Suspend mark. To log something upon reaching this breakpoint, check one of the following items: “Breakpoint hit” message, Stack trace, or Evaluate and log, as shown below.

Figure 1:Exception breakpoint configuration knobs

THU 106  Debugging Tips for Developers on IntelliJ IDEA-Mar-23-2021-07-11-47-01-AM

Figure 2:Configuring a tracepoint

As an aside, if you’ve ever been concerned about those cryptic breakpoint icons in the gutter area, IntelliJ IDEA offers official documentation describing them.

Stepping Through the Program

Stepping through the program is the essence of debugging: You stop normal execution and run the code line by line. Basic stepping actions—Step Over, Step Into, Step Out, Run to Cursor—are well-known to every developer, but there are more. These are hidden in the menu under Run → Debugging Actions, and include:

  • Smart Step Into: This command allows you to choose which particular method you want to step into if there are more than one of them on a line. Because argument lists are evaluated left-to-right, by using Smart Step Into you can choose to step into one of the rightmost methods of an argument list, thus skipping the invocations to the left.
  • Force Step Into: Have you ever tried to step into the println method of the System.out? If you try now, you’ll probably fail because some methods are skipped by this action as they don’t generally require debugging. But employing a Force Step Into action will allow you to step into them anyway.
  • Force Run to Cursor: The regular Run to Cursor action will stop on breakpoints it meets on its way. To skip them, try using Force Run to Cursor.
  • Drop Frame: Sometimes you miss an important breakpoint by accidentally clicking the Step Over button. You realize it moments later, but it’s too late: a bug may have already occurred and you have to restart the debugging session. Next time, try Drop Frame. It undoes the last frame of your program and restores the previous one, allowing you to repeat your computations.

Figure 3:Using the Smart Step Into option to skip m1() invocation

Marking Objects

Sometimes, during the debugging session you want to tag an object so you can later identify it among others. Marking does just that. Once a label is attached to an object, it will be marked until it exists. It can be useful in scenarios where you want to watch for a particular object’s state irrespective of the execution context. For instance, even if the object is not in a list of currently available values (locals, globals, or accessible fields), but exists somewhere in the heap, it could be watched by its label.

To mark an object, place a breakpoint somewhere that the object is accessible, right-click the object in the list of variables, choose Mark Object, and provide a label.

After that, the object can be watched by a ${label}_DebugLabel name anywhere in the program, as shown below.

Figure 4: Marking objects

Conditions and More

Regular conditional breakpoints are often used by experienced developers to skip unnecessary hits. A condition is just a piece of code that must return true for the breakpoint to be triggered. But even some experienced developers do not know that one can use object labels from the previous tip in breakpoint conditions!

Here are some interesting things you can do with conditions:

  • Self-destructible breakpoints: You guessed it; these self-deactivate after being triggered. To set one, mark the Remove once hit option in breakpoint properties.
  • Pass count breakpoints: Conversely, these are not triggered until being hit a requested number of times.
  • Dependent breakpoints: These are disabled until some other breakpoint is triggered, thus allowing you to create complex tree-like breakpoint hierarchies.
  • Filters: These allow you to further fine-grain the breakpoint’s triggering conditions, defining instances and classes that could trigger the breakpoint. Hits from other places will have no effect.

Figure 5: Breakpoint’s extended conditions

Custom Expressions and Watches

Not many know that you can watch both variables and custom expressions during a debugging session. If you add an arbitrary Java or Kotlin expression in the watch it will be re-evaluated automatically while you debug a program.

If you don’t want a persistent watch, and are just curious about expression value, try using Evaluate Expression and Quick Evaluate Expression from Run → Debugging Actions. The first accepts a piece of code as an input, and the latter will just evaluate a selected code. Remember the hotkeys for these two actions to further increase your debugging speed.

Inline Expressions

You may have noticed that the latest IDEA versions display values for local variables inline, right in the code editor. You can disable it by using Show values inline under Build, Execution, Deployment → Debugger → Data Views in the settings. But don’t rush it, this feature has a surprise: you can watch arbitrary expressions inline.

Figure 6: Inline expression

To do that, right click anywhere you want to display a watch and select Add Inline Watch from the context menu.

Streams Debugger

Java 8 streams are difficult to debug because they set a lazy sequence of operations and require you to insert additional breakpoints in the middle of the stream definition to analyze object propagation. Next time you have to debug a Java 8 stream, use Trace Current Stream Chain, a visual representation of your stream’s transformations, as shown below.

Figure 7: Streams debugger

Coroutines Debugger

If you are a Kotlin developer, chances are you are using coroutines. And you may agree that debugging them is extremely difficult for many reasons. The good news is that IntelliJ IDEA helps you debug them by providing a coroutines view during a debugging session.

THU 106  Debugging Tips for Developers on IntelliJ IDEA-Mar-23-2021-07-11-48-54-AM

Figure 8: Coroutines view

This view shows coroutines and their states at any point in time. Right-clicking the view allows you to create a coroutine dump—it’s like a thread dump, but for coroutines—to analyze later.

Debugging Serverless Applications with Thundra

Remote debugging of microservice applications is so challenging, it’s thought to be impossible.

Until now.

Thundra is a platform that provides end-to-end management of distributed applications across Kubernetes, containers, and virtual machines. Its IntelliJ IDEA plugin allows you to debug and trace remote applications like local ones, expanding your troubleshooting superpowers. Get started with Thundra.