skip to content

Given a chained dereference that throws, how does a Helpful NullPointerException pinpoint the exact failing part, and what does the message look like for fields, methods, and array access?

level: middleimportance: should knowfreq 42%

answer

  1. Two halves joined by 'because': operation + null expression
  2. Methods: 'return value of X is null'
  3. Fields: 'Cannot read field c because a.b is null'
  4. Arrays: load/store/length variants name the element type
  5. Built from failing bytecode + LocalVariableTable

basics

~10 s

The message names both the failed operation and the null sub-expression, e.g. 'Cannot read field x because y is null'. So on a long chain you instantly see which link was null.

solid answer

~40 s

On a chain like a.getB().getC().getD(), the JVM identifies which specific dereference faulted and phrases the message around it. The format pairs the operation with the cause: 'Cannot invoke "C.getD()" because the return value of "B.getC()" is null'. For field reads it says 'Cannot read field "c" because "a.b" is null'; for arrays, 'Cannot load from int array because "data" is null' or 'Cannot read the array length because ...'. It distinguishes a null local variable, a null field, a null method return value, and a null array. The JVM builds these descriptions from the failing bytecode instruction plus the method's debug information, so it can name local variables and reconstruct sub-expressions. That precision is the whole point: on one dense line you no longer guess which of several references was null.

code

java · 10 lines
java
int[] data = null;
int v = data[0];
// Cannot load from int array because "data" is null

String s = obj.toString().trim();   // obj.toString() returns null
// Cannot invoke "String.trim()" because the return value of
// "Object.toString()" is null

int n = list.size();                 // list is null
// Cannot invoke "java.util.List.size()" because "list" is null

go deeper

for a junior

Can read a helpful message and identify the null variable from the 'because ... is null' clause.

for a middle

Knows the operation+cause format and the variants for fields, method returns, and arrays.

for a senior

Explains the bytecode-instruction basis, the role of the LocalVariableTable, and the missing-debug-info caveat.

for a principal

Connects it to compiler debug-info policy, reasons about how chained-call APIs surface nulls, and how to use the message in incident triage.

## Anatomy of the message A Helpful NPE message always has two halves: 1. **The operation that failed** — what the code was trying to do with the null. 2. **The expression that was null** — the exact sub-expression whose value was `null`. They are joined by "because". Reading both tells you precisely what to fix. ## The chain problem Consider: ```java String city = order.getCustomer().getAddress().getCity(); ``` There are three method calls in a row. Any of them could return `null`, and the *next* call would then throw. A pre-14 NPE only gave the line. The helpful message instead reports the **innermost call that produced null** and the **call that tried to use it**: ``` Cannot invoke "Address.getCity()" because the return value of "Customer.getAddress()" is null ``` This says: `getAddress()` returned `null`, and the attempt to call `getCity()` on it failed. You now know exactly which method handed back null. ## Message variants by dereference kind The JVM phrases the message according to the **type of dereference** that faulted: - **Field read** (`a.b.c`): `Cannot read field "c" because "a.b" is null` - **Method call** (`a.b.foo()`): `Cannot invoke "T.foo()" because "a.b" is null`, or `because the return value of "..." is null` when the receiver came from a call. - **Array element read** (`arr[i]`): `Cannot load from int array because "arr" is null` (the element type appears, e.g. "object array"). - **Array element store** (`arr[i] = x`): `Cannot store to object array because "arr" is null`. - **Array length** (`arr.length`): `Cannot read the array length because "arr" is null`. - **Unboxing** a null wrapper into a primitive: a message describing the unboxing of the null value. - **throw null**: a message about throwing a null exception. ## How the JVM reconstructs the expression When the NPE is created, HotSpot inspects the **bytecode instruction** at the fault site. Each dereference compiles to a recognizable instruction: `getfield` (field read), `putfield` (field write), `invokevirtual`/`invokeinterface` (method call), `arraylength`, `aaload`/`iaload`/... (array reads), `aastore`/... (array writes). From the instruction kind it knows the *operation*. To name the *null expression*, it walks back through the preceding instructions and consults the class file's **LocalVariableTable** (names of local variables) and the constant pool (field/method names). If the source was compiled without local-variable debug info, locals may be shown generically (e.g. as a slot reference) rather than by name, but field and method names are still available from the constant pool. ## Why this beats the old behaviour The key win is **disambiguating multiple dereferences on one line**. You no longer split the chain into temporaries or attach a debugger just to learn *which* link was null — the JVM tells you directly, for every kind of dereference.

  • What happens to local-variable names in the message if the class was compiled without debug info?
    The JVM can't recover the source names from a missing LocalVariableTable, so locals may appear generically (e.g. a slot index) rather than by name; field and method names still come from the constant pool.
  • On 'a.getB().getC()' where getB() returns null, which call does the message blame?
    It blames the attempt to invoke getC() and reports that the return value of getB() was null — pinpointing getB() as the source of the null.

saying these in an interview costs you the question

  • Assuming it always blames the first reference — it names the specific faulting dereference and its null operand.
  • Thinking it cannot handle array access — it has dedicated messages for array load/store/length.
  • Expecting local-variable names even when compiled without debug info.

context