RizTech Academy logo
RizTech Academy
Exceptions and Error HandlingLesson 6 of 625 min

null, and designing NullPointerException out

NullPointerException is the most common exception in Java by a wide margin. Its inventor called null his "billion-dollar mistake", which is the sort of thing you can say once you have retired.

This lesson is not about catching it. Catching a NullPointerException is almost always wrong — it means something was null that your design did not expect, and the fix belongs where the null came from. This lesson is about designing so that it cannot happen.

Where nulls come from

Only four places, and knowing them tells you where to defend:

Source Example
A missing map entry map.get(absentKey)
A method that returns null on failure An old API, a JSON field that was absent
An uninitialised field Declared, never assigned, read early
An explicit null you passed Usually as a placeholder or a "no value" flag

Everything else — arrays, locals, parameters — is null because one of those four put it there.

Defend at the boundary, not everywhere

The instinct after being bitten is to check everything:

if (customer != null && customer.getAddress() != null
        && customer.getAddress().getPincode() != null) { ... }

That is unreadable, and it does not actually fix anything: it just decides, silently, to do nothing when the data is wrong.

The alternative is to make null impossible past one point:

record Subscriber(String name, String pincode) {
    Subscriber {
        Objects.requireNonNull(name, "name must not be null");
        Objects.requireNonNull(pincode, "pincode must not be null");
    }
}
name must not be null

Now every Subscriber in the system has a non-null name, and no code downstream needs to check. One assertion at construction replaces a hundred checks.

Objects.requireNonNull(x, "message") throws NullPointerException immediately, with your message, at the line where the null arrived — rather than three method calls later where it is used.

That is the principle: fail fast, at the boundary, with a message naming the field. The boundary is wherever data enters your code — a constructor, a parser, a controller.

The null-safe idioms

Worth memorising; each one removes a check.

Instead of Write Gives
s.equals("veg") "veg".equals(s) false, never throws
a.equals(b) with both uncertain Objects.equals(a, b) false, never throws
s.toString() String.valueOf(s) "null"
if (s == null) s = "veg" Objects.requireNonNullElse(s, "veg") the default
map.get(k) then a check map.getOrDefault(k, fallback) the fallback
if (s == null) Objects.isNull(s) / nonNull(s) reads better in a stream
returning null returning Optional or an empty collection nothing to check
literal first : false
Objects.equals: false
String.valueOf: null
concat is safe: plan: null
requireNonNullElse: veg

One that surprises people: string concatenation is null-safe. "plan: " + null gives "plan: null" rather than throwing. Useful in log messages, and a reason a null can travel a long way before anything notices.

Return empty, not null

static List<String> stopsFor(String area) {
    return ROUTES.getOrDefault(area, List.of());
}
2 stops for a known area
0 stops for an unknown one

A method returning a collection should never return null. Return an empty one. The caller then writes a loop that runs zero times instead of a null check, and the common case and the empty case are the same code.

The same applies to arrays — return new String[0] — and to strings, where an empty string is usually better than null.

For a single value that may be absent, return Optional, as module 6 covered. Never return Optional for a collection, because empty already says it.

Unboxing: the null you did not see

Map<String, Integer> counts = new HashMap<>();
int n = counts.get("Priya");
Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null

Nothing on that line looks like it could throw. The compiler inserted .intValue() to convert Integer to int, and that call is what failed.

Any assignment from a wrapper to a primitive is a hidden null check. The same trap appears in arithmetic (total + map.get(k)), in ternaries mixing Integer and int, and in for (int x : listOfIntegers).

The fix is getOrDefault, or keeping the variable as Integer and deciding what null means.

switch on null

String plan = null;
String r = switch (plan) {
    case "veg" -> "a";
    default -> "b";
};
old-style switch on null threw NullPointerException

The default does not save you. Since Java 21, pattern-matching switch lets you handle it explicitly:

String safe = switch (plan) {
    case null -> "no plan chosen";
    case "veg" -> "a";
    default -> "b";
};
with case null: no plan chosen

case null is better than a null check three lines above, because it is inside the construct that needs it and cannot be separated from it by a later edit.

Nulls in collections

List<String> withNull = new ArrayList<>(Arrays.asList("Priya", null));
withNull.stream().map(String::toUpperCase).toList();
ArrayList allows it: [Priya, null]
List.of rejects it
and it blows up downstream: null

ArrayList and HashMap allow nulls; List.of, Set.of and Map.of reject them at construction. Prefer the factories — failing where the null was introduced is far better than failing in a stream two hundred lines away, where the message is just null.

Note that last message. Not every NullPointerException gets a helpful description; when the null is inside a data structure rather than on the current line, you are back to reading the stack trace.

When a null is the right answer

Being fair to it:

  • At a boundary with an API that expects null. optional.orElse(null) handed to an older library is correct.
  • As a sentinel inside a tightly scoped algorithm where the meaning is obvious and the scope is ten lines.
  • In a field that genuinely has no value yet, where Optional as a field is worse — which module 6 argued it is.

Outside those, null is a decision not to say what you mean.

A checklist

  1. Validate parameters at constructors and public entry points with Objects.requireNonNull.
  2. Never return null from a method returning a collection, array or string.
  3. Return Optional for a single value that may be absent.
  4. Use getOrDefault rather than get on maps.
  5. Put the literal first in equals, or use Objects.equals.
  6. Use List.of and friends so nulls are rejected at the source.
  7. Handle case null in a pattern switch rather than checking beforehand.
  8. Do not catch NullPointerException. Fix where the null came from.

Check your work

Why is catching NullPointerException almost always wrong? It means the design allowed a null somewhere it was not expected. The fix belongs where the null was produced.

What are the four sources of null? A missing map entry; a method that returns null on failure; an uninitialised field; an explicit null you passed.

What does Objects.requireNonNull(x, "msg") buy you? Failure at the line the null arrived, with your message, instead of somewhere downstream with none.

Is "plan: " + null an error? No — string concatenation is null-safe and produces "plan: null". Which is why a null can travel a long way unnoticed.

What should a method returning a collection do when there is nothing? Return an empty collection, never null. The caller's loop then runs zero times.

Why does int n = map.get(k) throw for a missing key? The compiler inserts .intValue() to unbox, and that call is made on null. The message names intValue() even though you never wrote it.

Does a default branch protect a switch from a null selector? No. Only case null in a pattern switch handles it.

Which collection factories reject nulls? List.of, Set.of, Map.of. ArrayList and HashMap accept them.

Practice 3, the two failures. int n = counts.get("Priya") on an empty map gives Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null — which names the cause exactly. withNull.stream().map(String::toUpperCase) gives an NPE whose message is just null, because the null is inside the collection rather than on the failing line. The first is a five-second fix; the second needs the stack trace. That is the argument for List.of.

Practice 5, the chain of checks. The nested != null version and the requireNonNull-in-the-constructor version handle the same bad input. The difference is that the first silently does nothing and leaves the bad data in the system, while the second rejects it at the door and names the field. Only one of them leaves the rest of the codebase able to assume the data is good.

Practice

  1. Use every idiom. Write one line each for "veg".equals(s), Objects.equals, String.valueOf, requireNonNullElse, getOrDefault and Objects.isNull, all with a null input. Confirm none of them throw.

  2. Fail fast. Add Objects.requireNonNull with messages to a record's compact constructor and try to build an invalid one. Read the message.

  3. Cause the two NPE shapes. int n = emptyMap.get(k) for the helpful message, and a stream over a list containing null for the unhelpful one. Compare what each tells you.

  4. Return empty, not null. Write stopsFor(area) returning null for unknown areas, then have a caller loop over the result. Fix it to return List.of() and delete the caller's check.

  5. Compare two defences. Write the nested != null chain for a three-level object graph, then rewrite with requireNonNull at construction. Feed both the same bad input and describe what the system knows afterwards.

  6. Harder — make a whole flow null-free. Take the delivery-row parser from the custom exceptions lesson. Make it impossible for a Row with a null field to exist; make every lookup return an empty collection or an Optional; use case null where a switch needs it. Then deliberately feed it a CSV with empty columns and confirm that you get validation messages naming the fields rather than a single NullPointerException from somewhere in the middle.


That is module seven. You can read a stack trace in seconds, tell checked from unchecked and argue about why, write an exception whose message someone can act on, release resources without losing the exception that mattered, and design so that null does not get in.

Next module: files, dates and JSON — where every one of these failures happens for real.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship