Ch. 7 · Java

Java NullPointerException: Reading the Message and Fixing the Cause

Fix java.lang.NullPointerException by reading the helpful message, finding the null in the top stack frame and validating inputs early.

~8 min readbeginnerupdated Oct 4, 2026

Your program stopped and printed something like this:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "<local1>" is null
	at Main.main(Main.java:4)
Text

A NullPointerException means the JVM tried to use a reference that points at no object: it called a method on it, read or wrote one of its fields, indexed it as an array, unboxed it, threw it or synchronized on it. Since Java 15 the JVM fills in a “helpful” message (JEP 358) that names the exact operation and the expression that was null. Most of the detective work is already done for you if you read that message slowly.

Quick fix checklist

  • Read the message in two halves: the part before because is the operation that failed, the part after it is the expression that was null.
  • Open the first stack frame that belongs to your own code and go to that line number.
  • If the message says <local1> or <local4>, the class was compiled without local variable names; rebuild with -g or count the method’s variables to identify the slot.
  • Find where that value was supposed to be set: a field, a Map.get call, a method that can return null, an Integer being unboxed or an array slot that was never filled.
  • Fix the source: initialise the field, reject null at the boundary with Objects.requireNonNull, return Optional or a sensible default.
  • Do not catch NullPointerException to make the error disappear.

Before you start

You need JDK 17 or newer to see the messages shown here (they became the default in Java 15). On Java 8 or 11 you get a bare java.lang.NullPointerException with only a line number, so the techniques below still apply but you must work out the null expression yourself.

On a modern JDK with no message, check three things. Code may have thrown it explicitly (throw new NullPointerException()), which carries no computed message. The JVM may run with -XX:-ShowCodeDetailsInExceptionMessages. Or the JIT may be reusing a preallocated exception after the same NPE fired many times in hot code, which shows up in production logs as an NPE with no stack trace; -XX:-OmitStackTraceInFastThrow disables that.

You should also be comfortable with the difference between primitives (int, which can never be null) and references (Integer, String, any object type, which can).

Why it happens

A reference variable holds either a pointer to an object or the special value null. Bytecode instructions that need an object (invoking a method, getfield, putfield, loading from an array, reading arraylength, athrow, monitorenter) check the reference first. If it is null, the JVM throws NullPointerException at that instruction.

The helpful message is computed by analysing the bytecode around the failing instruction to reconstruct the source expression that pushed the null. That is why the wording follows the instruction type:

  • Cannot invoke "X.m()" because ... is null: a method call on null.
  • Cannot read field "f" because ... is null or Cannot assign field "f": field access.
  • Cannot load from object array because ... is null: the array itself is null.
  • Cannot read the array length because ... is null: arr.length on a null array.
  • Cannot enter synchronized block because ... is null.

The “because” part names a local variable, a field like "this.items", an array element, or the return value of "Customer.address()". Local variable names come from the class file’s local variable table, which only exists when the class was compiled with -g. Maven and Gradle builds normally include it; a bare javac Main.java does not, so locals appear as slots: <local0> is args in a static main, <local1> is the next declared variable, and so on (in instance methods slot 0 is this). The message also prints String and Object without their package but other classes in full, such as java.lang.Integer and java.util.Map.

Step-by-step walkthrough

Step 1: Reproduce and read the message

Here is a small program with an NPE that confuses many people, because the failing method does not appear in the source:

import java.util.Map;

public class Stock {
    public static void main(String[] args) {
        Map<String, Integer> stock = Map.of("apple", 12, "pear", 4);
        int bananas = stock.get("banana");
        System.out.println(bananas);
    }
}
java
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null
	at Stock.main(Stock.java:6)
Text

There is no intValue() call on line 6. The compiler inserted it because you assigned an Integer to an int (auto-unboxing). Map.get returns null for a missing key, and unboxing null fails. The message told you both facts.

Step 2: Find the right stack frame

The first at line is where the exception was thrown. If it is inside the JDK or a library (frames that start with java.base/ or a third-party package), keep reading downwards until you reach the first frame in your own package: that is where your code passed in or used the bad value. If the trace contains Caused by: sections, the last one is the original failure; the ones above it are wrappers.

When the top frame is your code but the message says <local2>, list the variables of that method in declaration order to identify slot 2, or rebuild with debug information and run again.

Step 3: Trace the null to its source

The line that throws is rarely the line that is wrong. Ask where the null value was created. The usual suspects:

import java.util.List;
import java.util.Map;

class Cart {
    private List<String> items;              // never initialised: null

    void add(String sku) {
        items.add(sku);                      // Cannot invoke "java.util.List.add(Object)" because "this.items" is null
    }
}

class Examples {
    static void run(Map<String, String> config, User user) {
        String[] names = new String[3];      // three null slots, not three empty strings
        System.out.println(names[0].trim()); // Cannot invoke "String.trim()" because "names[0]" is null

        String host = config.get("host");    // missing key returns null
        System.out.println(host.isBlank());

        String email = user.email();         // a getter that may return null
        System.out.println(email.toLowerCase());
    }
}

record User(String email) {}
java

Each case has the same shape: a value that was allowed to be null travelled some distance before something dereferenced it.

Step 4: Fix it where the null enters

Choose the fix based on what null means at that point:

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

class Cart {
    private final List<String> items = new ArrayList<>();   // initialise, never null

    void add(String sku) {
        items.add(Objects.requireNonNull(sku, "sku"));       // reject bad input at the boundary
    }
}

class Lookups {
    static int bananas(Map<String, Integer> stock) {
        return stock.getOrDefault("banana", 0);              // a real default exists
    }

    static Optional<String> host(Map<String, String> config) {
        return Optional.ofNullable(config.get("host"));      // absence is a normal result
    }
}
java

Objects.requireNonNull still throws an NPE, but immediately, with your message, at the point where the contract was broken. That turns a confusing failure three calls later into a clear one at the entry point. Use Optional for return values where “nothing found” is a normal answer, and a default only when the default is genuinely correct (zero stock for an unknown product is; an empty password is not).

Step 5: Prevent the next one

Validate in constructors (record compact constructors are ideal), return empty collections instead of null, and consider JSpecify’s @Nullable annotations with a checker such as NullAway, so the build flags risky calls before production does.

Worked scenario

A shipping service prints labels. It worked for months, then started failing for some orders:

record Address(String street, String city) {}
record Customer(String name, Address address) {}

class LabelPrinter {
    String label(Customer customer) {
        return customer.name() + "\n" + customer.address().city().toUpperCase();
    }
}
java
java.lang.NullPointerException: Cannot invoke "Address.city()" because the return value of "Customer.address()" is null
	at LabelPrinter.label(LabelPrinter.java:6)
Text

Diagnosis: the message says address() returned null, not city(). Checking the data, the failing customers had bought digital gift cards and never entered an address. A null address is a legitimate state, so the fix belongs in the model, not in a try/catch around the label.

import java.util.Objects;
import java.util.Optional;

record Address(String street, String city) {
    Address {
        Objects.requireNonNull(street, "street");
        Objects.requireNonNull(city, "city");
    }
}

record Customer(String name, Address address) {
    Customer {
        Objects.requireNonNull(name, "name");
    }

    Optional<Address> shippingAddress() {
        return Optional.ofNullable(address);
    }
}

class LabelPrinter {
    Optional<String> label(Customer customer) {
        return customer.shippingAddress()
            .map(a -> customer.name() + "\n" + a.city().toUpperCase());
    }
}
java

Now an address, when present, is guaranteed complete, and the caller is forced by the return type to decide what to do for customers with nothing to ship.

Common mistake

The tempting fix is to silence the error:

try {
    print(label(customer));
} catch (NullPointerException e) {
    // ignore
}
java

This hides every future NPE inside label too, including real bugs, and leaves the order unprocessed with no trace. A close cousin is sprinkling if (x != null) checks on every line until the exception stops; the program then silently skips work, and the null that caused it is still being produced upstream. Calling Optional.get() without checking is the same problem in a new wrapper: it throws NoSuchElementException instead.

Verify the behavior

Write tests for both the rejected input and the legitimate absent case:

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;

class LabelPrinterTest {
    @Test
    void rejectsCustomerWithoutName() {
        NullPointerException e = assertThrows(NullPointerException.class,
            () -> new Customer(null, null));
        assertEquals("name", e.getMessage());
    }

    @Test
    void digitalOnlyCustomerHasNoLabel() {
        Customer c = new Customer("Ada", null);
        assertTrue(assertDoesNotThrow(() -> new LabelPrinter().label(c)).isEmpty());
    }

    @Test
    void physicalCustomerGetsLabel() {
        Customer c = new Customer("Ada", new Address("1 Main St", "Leeds"));
        assertEquals("Ada\nLEEDS", new LabelPrinter().label(c).orElseThrow());
    }
}
java

Then replay a failing production order through the same path and confirm the NPE is gone from the log.

Interview exercise

The line int total = order.items().size() + order.bonus(); throws a NullPointerException, and bonus() returns Integer. How do you find which reference was null, and how would the answer differ on Java 11?

Answer and reasoning

There are three candidates: order, the return value of items(), and the return value of bonus() being unboxed. On Java 17 the message names the culprit directly. If it says Cannot invoke "java.lang.Integer.intValue()" because the return value of "Order.bonus()" is null, the bonus is missing; if it says Cannot invoke "java.util.List.size()", the list is. On Java 11 there is no message, so you would split the expression across lines or check each value in a debugger, because the line number alone cannot distinguish them. A strong answer then moves to the fix: items() should return an empty list rather than null, and bonus() should either be a primitive int defaulting to zero or return Optional<Integer> if “no bonus” means something different from zero.

Continue learning

More in Java

esc