Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Java  Module 1 – Start here: JDK 25 LTS and your first programs

IDEs, compiler errors and your first debug

Point VS Code or IntelliJ IDEA at JDK 25, read javac errors and stack traces line by line, and find a bug with breakpoints, stepping and jdb.

  • Beginner
  • 20 minutes
  • Examples run with Java 25.0.4.1 and Python 3.14.8
  • By MySmartCoPilot
Free: 20 minutes

What you will learn

  • Configure VS Code or IntelliJ IDEA to use the same JDK 25 as the terminal
  • Read a javac error message, fix it and compile again until the file compiles
  • Trace an exception to its cause and to the first line of your own code
  • Explain what a NullPointerException message names, with and without javac -g
  • Debug a program with breakpoints, stepping and watched variables, in an IDE and in jdb

Before you start

What you’ll learn · Before you start

What you will learn

  • Configure VS Code or IntelliJ IDEA to use the same JDK 25 as the terminal
  • Read a javac error message, fix it and compile again until the file compiles
  • Trace an exception to its cause and to the first line of your own code
  • Explain what a NullPointerException message names, with and without javac -g
  • Debug a program with breakpoints, stepping and watched variables, in an IDE and in jdb

Before you start

On this page

Java programs go wrong in three ways, and each has its own tool. A compile error: A message from the compiler that the code breaks a rule of the language, with the file, the line and the problem. A file with a compile error does not compile, so nothing of it runs. is the compiler refusing code that breaks the rules of the language: javac, or an IDE: An integrated development environment: an editor that also compiles, runs and debugs programs, such as IntelliJ IDEA, or VS Code with its Java extensions. while you type, names the file, the line and the problem, and nothing runs. An An object that a running program throws when something fails, such as a NumberFormatException. Unless code catches it, the program stops and prints where it happened. stops a program that compiled but failed while running, and its stack trace: What an uncaught exception prints: its class and message, then the calls that were open when it was thrown, the most recent first, each with its file and line, and any Caused by sections. names the line where that happened. A program that runs to the end and prints a wrong answer needs a debugger, which pauses it at a line you choose so that you can look at its variables and run it one line at a time.

Here you set up an IDE (an editor that also compiles, runs and debugs) on the same JDK 25 as your terminal, read real messages from javac and real stack traces, and then find a wrong average with breakpoint: A line marked in a debugger. The program pauses just before that line runs, so you can read its variables and go on one line at a time., in an IDE and in a terminal with jdb, the debugger that comes with every JDK.

The IDE and the terminal must use the same JDK 25

Two IDEs are common for Java: Visual Studio Code with Microsoft’s Extension Pack for Java, and IntelliJ IDEA from JetBrains. In both, the Java support runs on a Java runtime of its own, while your code is compiled with a JDK that you choose for each project, so the first job is to make that the same JDK 25 that your terminal uses.

  • VS Code: install the Extension Pack for Java, which includes Language Support for Java by Red Hat, Debugger for Java, Test Runner for Java, Maven for Java and Project Manager for Java, among others. The Red Hat extension embeds a Java runtime to run its language server; the JDK that compiles and runs your projects is a separate setting. To choose JDK 25, run Java: Configure Java Runtime from the Command Palette, or list the JDK in the setting java.configuration.runtimes.
  • IntelliJ IDEA: Java support is built in. The IDE runs on a bundled runtime that, in JetBrains’ words, “is not sufficient for developing Java applications”. To choose JDK 25, open File | Project Structure | Project Settings | Project and pick the JDK in the SDK list, or choose Download JDK and a vendor and version.

In VS Code, an entry of java.configuration.runtimes maps a Java version to a folder, for example { "name": "JavaSE-25", "path": "/path/to/jdk-25", "default": true }, and the entry marked default is used for source files that are not part of a Maven or Gradle project. For Maven and Gradle projects the JDK version comes from pom.xml or build.gradle, so that is where to change it. The Red Hat extension’s documentation lists code from Java 1.8 to Java 27 as supported.

Then check that both agree. This program prints the version of the runtime that runs it; run it once in the IDE and compare its line with java -version in a terminal, as the session does:

Version.java Java · Version.java
// Prints the version of the Java runtime that runs it. Run it in your IDE, then compare with java -version.
void main() {
    IO.println("This program runs on Java " + Runtime.version());
}
The terminal's java and the version a program sees Python · version_session.py
from session import run

# The JDK the terminal finds first, then the version that a program sees: in an IDE the two lines should name the same release.
run("""
java -version
java Version.java
""")

Output

# JDK in use: openjdk version "25.0.4.1" LTS
$ java -version
openjdk version "25.0.4.1" LTS
OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS)
OpenJDK 64-Bit Server VM Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS, mixed mode, sharing)
$ java Version.java
This program runs on Java 25.0.4.1+1-LTS

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 version_session.py

Both name release 25.0.4.1 here. When the IDE’s run names another release, the IDE and the terminal disagree, and the classic symptom is error: release version 25 not supported from an older compiler, which the lesson on installing JDK 25 reproduces.

Checkpoint

Check: One JDK for the IDE and the terminal

  1. Question 1 of 2 IntelliJ IDEA runs on a Java runtime that it bundles. What do you still need before it can build your projects?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A JDK chosen as the project's SDK, such as JDK 25

    In both IDEs the Java support runs on a runtime of its own, while your code is compiled with a JDK that you choose for each project. In IntelliJ IDEA that is the SDK in File | Project Structure | Project Settings | Project.

  2. Question 2 of 2 A program builds in your IDE, but javac in a terminal says error: release version 25 not supported. What does that tell you?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The terminal's javac belongs to a JDK older than 25

    Run a program in the IDE that prints Runtime.version() and compare it with java -version in a terminal. When they differ, an older compiler gives error: release version 25 not supported, as the lesson on installing JDK 25 shows.

A compile error names the file, the line and the broken rule

The diagram follows a source file through the two places where Java can stop a program: the compiler, before anything runs, and the JVM, while it runs. This section is about the first.

Bill.java goes to javac, which reports compile errors or writes Bill.class; java then runs it, ending in output or an exception.Bill.javajavac, or the IDEas you type, checksthe rules of the languageCompile errorBill.java:5: error:';' expectednothing runsBill.classjava runs the programException at run timea stack trace:at Bill.main(Bill.java:6)Outputa rule is brokenit compilessomething failsit runs to the end

Compile errors stop a program before it runs; exceptions stop it while it runs

Text description of the diagram

The diagram runs from top to bottom.

  1. The source file Bill.java goes to the compiler: javac in a terminal, or the IDE, which compiles as you type.
  2. If the code breaks a rule of the language, the compiler reports a compile error such as "Bill.java:5: error: ';' expected", with the file, the line and the problem. Nothing runs.
  3. If it compiles, the compiler writes Bill.class, and java runs the program.
  4. If something fails while the program runs, it stops with an exception and a stack trace, with lines such as "at Bill.main(Bill.java:6)". Otherwise it runs to the end and produces its output.

Here is a short program with three mistakes, compiled three times: as written, with the first mistake fixed, and with all three fixed.

step1/Bill.java: three mistakes Java · step1/Bill.java
public class Bill {
    public static void main(String[] args) {
        int plates = 3;
        int price = "120";
        int total = plates * price
        System.out.println("Total: " + totl);
    }
}
Fix the first error, compile again Python · errors_session.py
from session import run

# The same program three times: with three mistakes, with the first one fixed, and with all of them fixed.
run("""
javac -d out step1/Bill.java
javac -d out step2/Bill.java
javac -d out step3/Bill.java
java -cp out Bill
""", expect_failure=["javac -d out step1/Bill.java", "javac -d out step2/Bill.java"])

Output

# JDK in use: openjdk version "25.0.4.1" LTS
$ javac -d out step1/Bill.java
step1/Bill.java:5: error: ';' expected
        int total = plates * price
                                  ^
1 error
[exit status 1]
$ javac -d out step2/Bill.java
step2/Bill.java:4: error: incompatible types: String cannot be converted to int
        int price = "120";
                    ^
step2/Bill.java:6: error: cannot find symbol
        System.out.println("Total: " + totl);
                                       ^
  symbol:   variable totl
  location: class Bill
2 errors
[exit status 1]
$ javac -d out step3/Bill.java
$ java -cp out Bill
Total: 360

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 errors_session.py

Every message from javac has the same parts:

  • step1/Bill.java:5: error: ';' expected gives the file, the line number and what is wrong.
  • The next line repeats that source line, and the caret ^ below it marks the place where the compiler noticed the problem. For a missing semicolon that is the end of the line; sometimes the real mistake is on the line above.
  • The last line counts the errors.

The first compilation found one error, and the second, after fixing it, found two. OpenJDK’s overview of the compiler names its stages: the parser first turns the file into syntax trees, then Attr resolves every name and type, and Gen writes the class file. A missing semicolon stops the parser, so the other two mistakes were not looked for yet. Step through the three versions of the file, stage by stage:

Interactive · stage by stage Bill.java through javac, three times Step through the three versions of Bill.java. Each stage of the compiler runs only on a file that got through the stage before.
Bill.java through javac, three times, step 6 of 6. Showing Parse into syntax trees (step2 parses), Attr: names and types (step3 passes), Gen: write Bill.class, java -cp out Bill (Total: 360). In focus: java -cp out Bill.Parse into syntax treesstep2 parsesAttr: names and typesstep3 passesGen: write Bill.classbytecode into out/java -cp out BillTotal: 360

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. step1 does not parse. javac first parses step1/Bill.java into syntax trees. Line 5 has no semicolon, so parsing stops there with one error: ';' expected.
  2. The rest is not reached. A file that does not parse never reaches the next stage, so the two other mistakes, on lines 4 and 6, are not reported yet.
  3. step2 parses. With the semicolon added, step2/Bill.java parses, and Attr resolves every name and type in it.
  4. Two errors appear. Attr finds both remaining mistakes: "120" in quotes is a String, which cannot be stored in an int, and totl is a symbol it cannot find. 2 errors.
  5. step3 compiles. With 120 and total fixed, step3/Bill.java gets through Attr as well, and javac writes the class file out/Bill.class.
  6. It runs. java -cp out Bill runs the class and prints Total: 360, three plates at 120 each.

New errors appearing after a fix are therefore normal, not a sign that the fix was wrong. The two new messages:

  • incompatible types: String cannot be converted to int: "120" in quotes is text, and text cannot be stored in an int variable. The fix is the number 120.
  • cannot find symbol with symbol: variable totl and location: class Bill: no variable has that name. It is a typing mistake for total.

The habit that follows is simple: fix the first error, compile again, and read the next first error. An IDE compiles as you type and underlines the same problems in the editor, so most of them are fixed before you ever run javac.

Key idea

Fix the first error and compile again: a file that does not parse is never checked for names and types, so the next compilation may well report more errors than the last.

Checkpoint

Check: Reading a compile error

  1. Question 1 of 2 In the message step1/Bill.java:5: error: ';' expected, what does the 5 tell you?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The line on which the compiler noticed the problem

    Every javac message has the same parts: the file and the line, the problem, the source line repeated with a caret under the place where the compiler noticed it, and a count of the errors at the end.

  2. Question 2 of 2 Line 5 of step1/Bill.java is int total = plates * price, without its semicolon. Where did the caret of ';' expected point?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Just after price, at the end of line 5

    The caret under the repeated source line marks the place where the compiler noticed the problem. For a missing semicolon that is the end of the line; for some mistakes the real cause is on the line above.

A stack trace lists the open calls, newest first

This program compiles and starts, then stops with an exception:

Report.java Java · Report.java
public class Report {
    public static void main(String[] args) {
        String[] lines = {"72", "85", "6O", "91"};
        System.out.println("Average: " + average(lines));
    }

    static double average(String[] lines) {
        int total = 0;
        for (int i = 0; i < lines.length; i++) {
            total += parseMark(lines[i], i + 1);
        }
        return (double) total / lines.length;
    }

    static int parseMark(String text, int lineNumber) {
        try {
            return Integer.parseInt(text.strip());
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("line " + lineNumber + " is not a mark: " + text, e);
        }
    }
}
An exception with its cause Python · trace_session.py
from session import run

run("""
javac -d out Report.java
java -cp out Report
""", expect_failure=["java -cp out Report"])

Output

# JDK in use: openjdk version "25.0.4.1" LTS
$ javac -d out Report.java
$ java -cp out Report
Exception in thread "main" java.lang.IllegalArgumentException: line 3 is not a mark: 6O
	at Report.parseMark(Report.java:19)
	at Report.average(Report.java:10)
	at Report.main(Report.java:4)
Caused by: java.lang.NumberFormatException: For input string: "6O"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
	at java.base/java.lang.Integer.parseInt(Integer.java:565)
	at java.base/java.lang.Integer.parseInt(Integer.java:662)
	at Report.parseMark(Report.java:17)
	... 2 more
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 trace_session.py

Step through the run first: the calls going down from main, and the exception coming back up through them, at the lines the trace names:

Interactive · message by message How Report.java's exception climbed the call stack Step through the run. Each arrow is a call going down or the exception coming back up, at the line the trace names.
How Report.java's exception climbed the call stack, step 8 of 8. Messages so far: main to average: average(lines); average to parseMark: "72", then "85"; average to parseMark: parseMark("6O", 3); parseMark to average: IllegalArgumentException; average to main: IllegalArgumentException.mainReport.java line 4averagetotal 157parseMarkthrows at line 19average(lines)"72", then "85"parseMark("6O", 3)IllegalArgumentExceptionIllegalArgumentException

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. main calls average. main calls average(lines) on line 4, with the four marks "72", "85", "6O" and "91".
  2. Two marks parse. On line 10 the loop calls parseMark for each line in turn. "72" and "85" parse, and the total is 157.
  3. The third mark. The third call gets "6O": a six followed by the letter O, not the digit zero.
  4. parseInt fails. On line 17, Integer.parseInt throws a NumberFormatException with the message For input string: "6O".
  5. A clearer exception. The catch block on line 19 throws an IllegalArgumentException, line 3 is not a mark: 6O, with the NumberFormatException attached as its cause.
  6. Out of average. average has no catch for it, so the exception leaves average at line 10, the line of the call that was still open.
  7. Out of main. main has no catch either, so the exception leaves main at line 4. Nothing is left to catch it: the JVM prints the stack trace and the program stops.
  8. The trace, newest first. The trace lists the open calls from the newest to the oldest: parseMark at line 19, average at 10, main at 4. Caused by then shows the NumberFormatException with its own frames.

Then read the trace from the top:

  1. The first line names the exception and its message: an IllegalArgumentException saying line 3 is not a mark: 6O.
  2. The at lines are the call stack when it was thrown, the most recent call first: parseMark at line 19 of Report.java, which average called from line 10, which main called from line 4.
  3. Caused by is the exception that started it all. parseMark caught a NumberFormatException and threw the clearer exception with the original attached as its cause. The cause’s message, For input string: "6O", shows the real problem: the third mark holds the letter O, not the digit zero.
  4. Its first three frames are inside the JDK (java.base/…). Skip them to the first frame in your own code, Report.parseMark(Report.java:17), the line that called Integer.parseInt.
  5. ... 2 more means that the cause’s remaining two frames are the same as the last two frames of the exception above (average and main), so they are not printed twice; the documentation of Throwable.printStackTrace describes this shorthand.

So the usual reading order is: the first line for what happened, the last Caused by for why, and the first frame in your own files for where.

Key idea

A stack trace answers three questions in three places: the first line says what happened, the last Caused by says why, and the first frame in your own code says where to look.

Checkpoint

Check: Reading a stack trace

  1. Question 1 of 2 Which line of Report.java's stack trace tells you why the program failed?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Caused by: java.lang.NumberFormatException: For input string: "6O"

    Read a trace in three steps: the first line for what happened, the last Caused by for why, and the first frame in your own files for where. Here the cause's message, For input string: "6O", shows the real problem.

  2. Question 2 of 2 A trace shows at Shop.price(Shop.java:30), then at Shop.total(Shop.java:12), then at Shop.main(Shop.java:5). Which method called price?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: total, at line 12 of Shop.java

    The at lines are the call stack at the moment of the throw, newest first. Reading down, each method was called by the one below it: main called total at line 5, and total called price at line 12.

A NullPointerException says what was null

A NullPointerException means that code used a reference that pointed to no object. JEP 358 taught the JVM to say which reference was null, and the JDK 25 documentation of java lists the option behind it, -XX:+ShowCodeDetailsInExceptionMessages, as enabled by default. How much the message can say depends on how the class was compiled:

Cities.java Java · Cities.java
import java.util.Map;

public class Cities {
    public static void main(String[] args) {
        Map<String, String> capitals = Map.of("Karnataka", "Bengaluru", "Kerala", "Thiruvananthapuram");
        String state = "Goa";
        String city = capitals.get(state);
        System.out.println(state + ": " + city.length() + " letters");
    }
}
Cities2.java: the same lookup without a variable Java · Cities2.java
import java.util.Map;

public class Cities2 {
    public static void main(String[] args) {
        Map<String, String> capitals = Map.of("Karnataka", "Bengaluru", "Kerala", "Thiruvananthapuram");
        String state = "Goa";
        System.out.println(state + ": " + capitals.get(state).length() + " letters");
    }
}
One NullPointerException, four ways Python · npe_session.py
from session import run

# The same NullPointerException from the source launcher, from javac's default class file and from javac -g; the
# table of local variable names that only -g writes; and a null that comes straight from a method call.
run("""
java Cities.java
javac -d out Cities.java
java -cp out Cities
javac -g -d out-g Cities.java
java -cp out-g Cities
javap -c -l -cp out Cities | grep LocalVariableTable || echo "(no LocalVariableTable)"
javap -c -l -cp out-g Cities | sed -n '/void main/,$p' | sed -n '/LocalVariableTable/,$p'
java Cities2.java
""", expect_failure=["java Cities.java", "java -cp out Cities", "java -cp out-g Cities", "java Cities2.java"])

Output

# JDK in use: openjdk version "25.0.4.1" LTS
$ java Cities.java
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "<local3>" is null
	at Cities.main(Cities.java:8)
[exit status 1]
$ javac -d out Cities.java
$ java -cp out Cities
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "<local3>" is null
	at Cities.main(Cities.java:8)
[exit status 1]
$ javac -g -d out-g Cities.java
$ java -cp out-g Cities
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "city" is null
	at Cities.main(Cities.java:8)
[exit status 1]
$ javap -c -l -cp out Cities | grep LocalVariableTable || echo "(no LocalVariableTable)"
(no LocalVariableTable)
$ javap -c -l -cp out-g Cities | sed -n '/void main/,$p' | sed -n '/LocalVariableTable/,$p'
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      43     0  args   [Ljava/lang/String;
           12      31     1 capitals   Ljava/util/Map;
           15      28     2 state   Ljava/lang/String;
           26      17     3  city   Ljava/lang/String;
}
$ java Cities2.java
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because the return value of "java.util.Map.get(Object)" is null
	at Cities2.main(Cities2.java:7)
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 npe_session.py

  • capitals.get("Goa") returned null because the map has no such key, and city.length() then failed. The message is precise: Cannot invoke "String.length()" because "<local3>" is null.
  • <local3> is a slot number, not a name: here the slots are args 0, capitals 1, state 2 and city 3, as the table at the end of the session shows. By default javac writes only line numbers and the source file name as debug information: Tables that javac writes into a class file besides the bytecode: line numbers and the source file's name by default, and local variable names only with javac -g. Debuggers and exception messages use them.; local variable names are written only with javac -g. Without -g, as from the source launcher or a plain javac, the JVM knows only the number.
  • Compiled with -g, the class file holds a LocalVariableTable, which javap -c -l printed, and the message names city.
  • Cities2.java calls length() on the result of get directly, and the message names the method whose result was null, the return value of "java.util.Map.get(Object)", whatever the compiler options.

Security

These messages describe your code: variable names, field names and method names. That is what you want in your own logs, and what you do not want in front of the users of a service. Log exceptions where only you can read them, and show users a plain message instead of e.getMessage() or a stack trace.

Checkpoint

Check: Helpful NullPointerException messages

  1. Question 1 of 2 Cities2.java calls capitals.get(state).length() directly. Why does its message name java.util.Map.get(Object) although it was not compiled with -g?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The null came straight from a call, and the message names the method that returned it

    Helpful NullPointerException messages describe the expression that was null. A local variable can be named only when the class has a LocalVariableTable, from javac -g, but the return value of a method is named in any class.

  2. Question 2 of 2 Why should a web service not show its users the message of a NullPointerException?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The message describes your code: names of variables, fields and methods

    Log exceptions where only you can read them, and show users a plain message instead of e.getMessage() or a stack trace.

A breakpoint stops the program before its line runs

This program should print the average of five marks, 73.8, but it prints 59.4:

buggy/Average.java Java · buggy/Average.java
public class Average {
    public static void main(String[] args) {
        int[] marks = {72, 85, 64, 91, 57};
        double average = average(marks);
        System.out.println("Average: " + average);
    }

    static double average(int[] marks) {
        int total = 0;
        for (int i = 1; i < marks.length; i++) {
            total += marks[i];
        }
        return (double) total / marks.length;
    }
}

Reading the code may or may not show the bug. A debugger shows it for certain. In an IDE:

  1. Set a breakpoint on line 11, total += marks[i];, by clicking in the margin next to the line number (the gutter, in IntelliJ IDEA). In VS Code, F9 does the same on the current line.
  2. Start the program in debug mode: in VS Code press F5, or use the Debug link that the editor shows for main; in IntelliJ IDEA click the run icon in the gutter next to main and choose Debug.
  3. The program stops before line 11 runs, and the variables view shows marks, total and i with their values. Here total is 0 and i is already 1, so marks[0], the 72, can never be added: the loop starts at the wrong index.
  4. Step to follow it: Step Over runs one line, Step Into enters a method that the line calls, and Step Out runs to the end of the current method. Continue (VS Code) or Resume (IntelliJ IDEA) runs to the next breakpoint.

Step through such a session. The values are the ones the debugger printed at this breakpoint in the jdb session of the next section:

Interactive · message by message A breakpoint on line 11 of Average.java Step through a debugging session. The sub-label of the program shows the variables the debugger shows when it stops.
A breakpoint on line 11 of Average.java, step 7 of 7. Messages so far: You to Debugger: breakpoint at line 11; Debugger to Average.java: run in debug mode; Average.java to Debugger: stopped at line 11; You to Debugger: Continue; Average.java to You: Average: 59.4; Average.java to You: Average: 73.8.Youin the IDEDebuggerVS Code or IntelliJ IDEAAverage.javafinishedbreakpoint at line 11run in debug modestopped at line 11ContinueAverage: 59.4Average: 73.8

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Set the breakpoint. Click the margin next to line 11, total += marks[i];, or press F9 on it in VS Code. Nothing happens yet: a breakpoint waits for the program to reach its line.
  2. Start in debug mode. Start the program in debug mode, with F5 in VS Code or Debug in IntelliJ IDEA. The debugger starts the JVM, and main calls average.
  3. Stopped before line 11. The program reaches line 11 and stops before the line runs. The variables view shows total = 0 and i = 1.
  4. The bug, seen. i is already 1 on the first pass, so marks[0], the 72, can never be added. The loop starts at the wrong index.
  5. Continue. Continue runs to the next stop: the same breakpoint on the next pass, where total = 85, which is marks[1], and i = 2.
  6. Let it finish. Remove the breakpoint and continue, and the program prints Average: 59.4, the wrong answer that the variables have explained.
  7. The fix. With the loop starting at i = 0, every mark is added, and the program prints Average: 73.8.

Both IDEs also have watch expressions, which they evaluate at every pause (marks[i] or total + marks[i], say), and conditional breakpoints, which stop only when a condition you type is true, such as i == 4, so a loop of a million turns can stop at the one that matters.

To VS Code IntelliJ IDEA
Set a breakpoint Click the margin, or F9 Click the gutter
Start debugging F5, or the Debug link for main Run icon in the gutter, then Debug
Run one line Step Over, F10 Step Over
Enter a call Step Into, F11 Step Into
Leave the method Step Out, Shift+F11 Step Out
Run to the next stop Continue, F5 Resume
See variables Variables and Watch views Variables tab
See the call stack Call Stack view Frames tab

Checkpoint

Check: Breakpoints and stepping

  1. Question 1 of 2 Stopped at the breakpoint on line 11 for the first time, the debugger shows total = 0 and i = 1. What does that tell you?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The loop started at index 1, so marks[0] is never added

    A breakpoint shows the state before its line runs. On the first pass of a loop meant to start at 0, i = 1 is the bug itself: the loop starts at the wrong index, and the average comes out as 59.4 instead of 73.8.

  2. Question 2 of 2 A loop runs a million times, and the bug appears only when i is 999999. What do you set?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A conditional breakpoint with the condition i == 999999

    Both IDEs have conditional breakpoints, which stop only when a condition you type is true. A watch expression is evaluated at every pause, but something else has to make the program pause.

jdb gives the same control in a terminal

jdb is the JDK’s command-line debugger: useful on a server or a lab computer with no IDE, and a clear way to see what an IDE does behind its buttons. The rows of the table become commands: stop at sets a breakpoint, run starts the program, next, step and step up run one line, enter a call and leave the method, cont runs to the next stop, locals and print show variables, and where shows the call stack. Its own help describes them:

What jdb says about its commands Python · jdb_help_session.py
from session import run

# jdb describes its own commands; these are the ones the lesson uses.
run("""
printf 'help\\nexit\\n' | jdb | grep -E -A1 '^stop '
printf 'help\\nexit\\n' | jdb | grep -E '^(run |next|step|cont|locals|print|where |clear <class id>:)'
""")

Output

# JDK in use: openjdk version "25.0.4.1" LTS
$ printf 'help\nexit\n' | jdb | grep -E -A1 '^stop '
stop [go|thread] [<thread_id>] <at|in> <location>
                          -- set a breakpoint
$ printf 'help\nexit\n' | jdb | grep -E '^(run |next|step|cont|locals|print|where |clear <class id>:)'
run [class [args]]        -- start execution of application's main class
where [<thread id> | all] -- dump a thread's stack
print <expr>              -- print value of expression
locals                    -- print all local variables in current stack frame
clear <class id>:<line>   -- clear a breakpoint at a line
step                      -- execute current line
step up                   -- execute until the current method returns to its caller
stepi                     -- execute current instruction
next                      -- step one line (step OVER calls)
cont                      -- continue execution from breakpoint

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 jdb_help_session.py

Here is the bug hunt, typed into jdb:

Find the wrong average with jdb Python · debug_session.py
from jdb_session import jdb
from session import run

# The wrong average, then jdb: stop inside the loop, look at the variables, step, and find where the loop started.
run("""
javac -g -d out buggy/Average.java
java -cp out Average
""")
jdb([
    "stop at Average:11",
    "run",
    "locals",
    "cont",
    "print total",
    "print i",
    "where",
    "clear Average:11",
    "cont",
], ["-classpath", "out", "Average"])
run("""
javac -g -d out fixed/Average.java
java -cp out Average
""")

Output

# JDK in use: openjdk version "25.0.4.1" LTS
$ javac -g -d out buggy/Average.java
$ java -cp out Average
Average: 59.4
$ jdb -classpath out Average
Initializing jdb ...
> stop at Average:11
Deferring breakpoint Average:11.
It will be set after the class is loaded.
> run
run Average
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
> 
VM Started: Set deferred breakpoint Average:11

Breakpoint hit: "thread=main", Average.average(), line=11 bci=10

main[1] locals
Method arguments:
marks = instance of int[5] (id=432)
Local variables:
total = 0
i = 1
main[1] cont
> 
Breakpoint hit: "thread=main", Average.average(), line=11 bci=10

main[1] print total
 total = 85
main[1] print i
 i = 2
main[1] where
  [1] Average.average (Average.java:11)
  [2] Average.main (Average.java:4)
main[1] clear Average:11
Removed: breakpoint Average:11
main[1] cont
> Average: 59.4

The application exited
# JDK in use: openjdk version "25.0.4.1" LTS
$ javac -g -d out fixed/Average.java
$ java -cp out Average
Average: 73.8

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 debug_session.py

  • javac -g compiled the class with its debug information, which a debugger needs to show variables by name.
  • stop at Average:11 set the breakpoint. The class was not loaded yet, so jdb deferred it until it was.
  • run started the program in a second JVM, connected to jdb, and the breakpoint stopped it: Breakpoint hit with the thread, the method and the line. The prompt changed to main[1], the stopped thread.
  • locals showed total = 0 and i = 1 on the very first pass: the bug, found without guessing.
  • cont ran to the next pass, where print total showed 85, which is marks[1], and where showed that average had been called from line 4 of main.
  • clear Average:11 removed the breakpoint and cont let the program finish. After the fix, for (int i = 0; ...), the program printed 73.8.

Without -g the debugger can still stop at a line, but it cannot show any local variable:

The same class compiled without -g Python · nodebug_session.py
from jdb_session import jdb
from session import run

# The same class compiled without -g: jdb still stops at the line, but it cannot show the local variables.
run("""
javac -d out-plain buggy/Average.java
""")
jdb([
    "stop at Average:11",
    "run",
    "locals",
    "print total",
    "clear Average:11",
    "cont",
], ["-classpath", "out-plain", "Average"])

Output

# JDK in use: openjdk version "25.0.4.1" LTS
$ javac -d out-plain buggy/Average.java
$ jdb -classpath out-plain Average
Initializing jdb ...
> stop at Average:11
Deferring breakpoint Average:11.
It will be set after the class is loaded.
> run
run Average
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
> 
VM Started: Set deferred breakpoint Average:11

Breakpoint hit: "thread=main", Average.average(), line=11 bci=10

main[1] locals
Local variable information not available.  Compile with -g to generate variable information
main[1] print total
com.sun.tools.example.debug.expr.ParseException: Name unknown: total
 total = null
main[1] clear Average:11
Removed: breakpoint Average:11
main[1] cont
> Average: 59.4

The application exited

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 nodebug_session.py

jdb said so itself: Local variable information not available. Compile with -g to generate variable information. It is the same debug information that let the NullPointerException name city, so compile with -g whenever you expect to debug.

Checkpoint

Check: Debugging in a terminal with jdb

  1. Question 1 of 2 jdb stops at your breakpoint, but locals says Local variable information not available. What do you change?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Compile the class with javac -g

    jdb said so itself: Compile with -g to generate variable information. It is the same debug information that let the NullPointerException name city.

  2. Question 2 of 2 Which jdb command shows the call stack of the stopped thread?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: where

    where printed [1] Average.average (Average.java:11) and [2] Average.main (Average.java:4): the same call stack that an IDE's Call Stack view or Frames tab shows.

Practise: fix five programs

Each of these five programs has one mistake. Fix them so that each one compiles, runs without an exception and prints exactly its expected output:

  • Fix1.java prints Welcome to Kochi.
  • Fix2.java prints Seats left: 2.
  • Fix3.java prints Total: 165.
  • Fix4.java prints 47 passes: true.
  • Fix5.java prints 1: 72, 2: 85 and 3: 64, on three lines.
The five programs

Java (Fix1.java) · exercise/Fix1.java

// 1. Prints a welcome line. It does not compile yet.
void main() {
    String city = "Kochi"
    IO.println("Welcome to " + city);
}

Java (Fix2.java) · exercise/Fix2.java

// 2. Prints how many seats are left. It does not compile yet.
void main() {
    int booked = 38;
    int capacity = "40";
    IO.println("Seats left: " + (capacity - booked));
}

Java (Fix3.java) · exercise/Fix3.java

// 3. Prints a bill total. It does not compile yet.
void main() {
    int total = 120 + 45;
    IO.prinltn("Total: " + total);
}

Java (Fix4.java) · exercise/Fix4.java

// 4. Says whether a mark passes. It does not compile yet.
void main() {
    IO.println("47 passes: " + passes(47));
}

boolean passes(int mark) {
    if (mark >= 35) {
        return true;
    }
}

Java (Fix5.java) · exercise/Fix5.java

// 5. Prints each mark with its position. It compiles, but it stops with an exception.
void main() {
    int[] marks = {72, 85, 64};
    for (int i = 0; i <= marks.length; i++) {
        IO.println((i + 1) + ": " + marks[i]);
    }
}

The session runs a sample test for each: the program must exit without an error (&& runs the next command only then), and diff must find no difference from the expected output. As given, all five fail, and the messages above each [exit status 1] are the place to start:

The sample tests before you start Python · exercise_session.py
from session import run

# The sample tests: each program must run without an error and print exactly its expected output.
TESTS = """
java exercise/Fix1.java > got.txt && diff exercise/expected-1.txt got.txt && echo PASS
java exercise/Fix2.java > got.txt && diff exercise/expected-2.txt got.txt && echo PASS
java exercise/Fix3.java > got.txt && diff exercise/expected-3.txt got.txt && echo PASS
java exercise/Fix4.java > got.txt && diff exercise/expected-4.txt got.txt && echo PASS
java exercise/Fix5.java > got.txt && diff exercise/expected-5.txt got.txt && echo PASS
"""
run(TESTS, expect_failure=[line.strip() for line in TESTS.strip().splitlines()])

Output

# JDK in use: openjdk version "25.0.4.1" LTS
$ java exercise/Fix1.java > got.txt && diff exercise/expected-1.txt got.txt && echo PASS
exercise/Fix1.java:3: error: ';' expected
    String city = "Kochi"
                         ^
1 error
error: compilation failed
[exit status 1]
$ java exercise/Fix2.java > got.txt && diff exercise/expected-2.txt got.txt && echo PASS
exercise/Fix2.java:4: error: incompatible types: String cannot be converted to int
    int capacity = "40";
                   ^
1 error
error: compilation failed
[exit status 1]
$ java exercise/Fix3.java > got.txt && diff exercise/expected-3.txt got.txt && echo PASS
exercise/Fix3.java:4: error: cannot find symbol
    IO.prinltn("Total: " + total);
      ^
  symbol:   method prinltn(String)
  location: class IO
1 error
error: compilation failed
[exit status 1]
$ java exercise/Fix4.java > got.txt && diff exercise/expected-4.txt got.txt && echo PASS
exercise/Fix4.java:10: error: missing return statement
}
^
1 error
error: compilation failed
[exit status 1]
$ java exercise/Fix5.java > got.txt && diff exercise/expected-5.txt got.txt && echo PASS
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
	at Fix5.main(Fix5.java:5)
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 exercise_session.py

First hint: read only the first line of each message, the file, the line number and the problem. For Fix4.java, ask what passes returns when the mark is below 35. For Fix5.java, the exception names the index that was used and the length of the array; compare them with the loop’s condition.

Text Diff Paste a program's expected output and what it printed, and see the exact characters that differ.

With all five fixed, go over the six ideas of the lesson once more. The questions start where you are weakest, and each one shows why its answers are right or wrong as soon as you choose:

Practice that adapts to you

Practise this lesson

12 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 In VS Code, which setting lists the JDKs your Java code can use, with one of them marked default?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: java.configuration.runtimes

    An entry of java.configuration.runtimes maps a Java version to a folder, for example JavaSE-25 to the folder of JDK 25, and the entry marked default is used for source files that are not part of a Maven or Gradle project.

  2. Question 2 of 12 In VS Code, a Maven project still compiles for an older Java version, although JDK 25 is the default runtime. Where do you change it?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: In the project's pom.xml

    Source files outside a build use the default entry of java.configuration.runtimes, while Maven and Gradle projects take their Java version from pom.xml or build.gradle, which is where to change it.

  3. Question 3 of 12 javac reports 1 error, a missing semicolon. You fix it and compile again, and it reports 2 errors. What does that mean?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: The fix was right; the compiler could check types and names only once the file parsed

    A missing semicolon stops the compiler while it reads the structure of the file. Once the file parses, it checks types and names and finds the mistakes it could not see before, as step2/Bill.java showed: an incompatible type and a misspelled variable.

  4. Question 4 of 12 What does error: incompatible types: String cannot be converted to int say about int price = "120";?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: The value in quotes is text, and text cannot be stored in an int variable

    "120" is a String literal. Java does not convert text to a number by itself; write the number 120, or convert a string at run time with Integer.parseInt.

  5. Question 5 of 12 What does ... 2 more at the end of a Caused by section mean?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: The cause's remaining two frames are the same as the last two frames of the trace above

    The documentation of Throwable.printStackTrace describes the shorthand: frames that a cause shares with the exception it caused are printed once, and the cause's section ends with ... n more.

  6. Question 6 of 12 A stack trace ends with Caused by: java.lang.NumberFormatException and frames inside java.base/java.lang.Integer.parseInt, then at Report.parseMark(Report.java:17). Where do you look first?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: Line 17 of Report.java, the first frame in your own code

    Frames inside the JDK show where the failure was detected, but the cause is in the arguments your code passed. The first frame in your own files is the line that called parseInt with the bad text.

  7. Question 7 of 12 Why does a NullPointerException message say because "<local3>" is null instead of naming the variable?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: The class was compiled without javac -g, so it holds no local variable names

    By default javac writes only line numbers and the source file name. The JVM then knows a local variable only by its slot number. Compiled with javac -g, the same failure named city.

  8. Question 8 of 12 static void main(String[] args) declares String first = …; and then String second = …;, and second.length() fails. Compiled without -g, which placeholder does the message name?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: <local2>

    Without -g the message names a local variable by its slot. In Cities.java the LocalVariableTable listed args 0, capitals 1, state 2 and city 3; here args is 0, first is 1 and second is 2.

  9. Question 9 of 12 A breakpoint is set on the line total += marks[i];. When the program stops there, has that line run?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: No: the program stops before the line runs

    A debugger stops before the line runs, which is why jdb showed total = 0 at the first stop: nothing had been added yet. Step Over then runs the line and stops again.

  10. Question 10 of 12 The program is stopped on a line that calls average(marks). You want to watch what happens inside average. Which action do you use?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: Step Into (F11 in VS Code, step in jdb)

    Step Into enters the method that the line calls. Step Over would run the whole call and stop at the next line, and Step Out finishes the current method.

  11. Question 11 of 12 Why did jdb answer stop at Average:11 with Deferring breakpoint Average:11?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: The class Average was not loaded yet, so jdb set the breakpoint once it was

    Before run, the program has not started and no class of it is loaded. jdb keeps the breakpoint and sets it when Average is loaded, then reports VM Started: Set deferred breakpoint Average:11.

  12. Question 12 of 12 On a lab computer with no IDE, you want to see total at line 11 of Average.java. Which plan works?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: javac -g, then jdb with stop at Average:11, run and print total

    The steps of the lesson's session: compile with javac -g so that the names are in the class file, set the breakpoint, start the program with run, and read the variable with print or locals when it stops.

Interview questions

Fresher · How do you read a Java stack trace to find the cause of an exception?

Start at the first line: it names the exception class and its message, which often states the problem outright. The at lines below it are the call stack at the moment of the throw, the most recent call first, each with the class, the method, the file and the line number. If there is a Caused by: section, the last one is usually the root cause, because code often catches a low-level exception and throws a clearer one with the original attached. Skip the frames inside the JDK or a library and find the first frame in your own code: that line is where to look. ... 2 more means the remaining frames are the same as in the trace above. A good answer adds that an exception message can be null, and that NullPointerException messages show a local variable’s name only when the class was compiled with javac -g.

Key takeaways

  • Point the IDE at the same JDK 25 as the terminal: Java: Configure Java Runtime or java.configuration.runtimes in VS Code, the project SDK in IntelliJ IDEA.
  • A javac message gives the file, the line, the problem and a caret; fix the first one and compile again, because more errors can appear once the file parses.
  • Read a stack trace top-down: what happened, the last Caused by for why, the first frame in your own code for where.
  • Helpful NullPointerException messages name the null expression; local variables appear by name only in classes compiled with javac -g.
  • A breakpoint pauses the program before a line runs; step over, into or out, and read the variables instead of guessing.
  • jdb gives the same control in a terminal: stop at, run, locals, print, next, step, where, cont.

Terms in this lesson

breakpoint
A line marked in a debugger. The program pauses just before that line runs, so you can read its variables and go on one line at a time.
compile error
A message from the compiler that the code breaks a rule of the language, with the file, the line and the problem. A file with a compile error does not compile, so nothing of it runs.
debug information
Tables that javac writes into a class file besides the bytecode: line numbers and the source file's name by default, and local variable names only with javac -g. Debuggers and exception messages use them.
IDE
An integrated development environment: an editor that also compiles, runs and debugs programs, such as IntelliJ IDEA, or VS Code with its Java extensions.
stack trace
What an uncaught exception prints: its class and message, then the calls that were open when it was thrown, the most recent first, each with its file and line, and any Caused by sections.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.