On This Page
A switch has always thrown on a null selector, and default has never been the thing that catches it. Java 21 finally gave you a way to say what null should do, and it's the first time most people find out the old behaviour was a rule at all.
Here's the part that sends people searching. Four different switches, same input, and you get four different-looking failures:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.hashCode()" because "<local1>" is null
at Repro.byString(Repro.java:8)
at Repro.main(Repro.java:44)
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Repro$Color.ordinal()" because "<parameter1>" is null
at Repro.byEnum(Repro.java:14)
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "<parameter1>" is null
at Repro.byBoxed(Repro.java:17)
Exception in thread "main" java.lang.NullPointerException
at java.base/java.util.Objects.requireNonNull(Objects.java:233)
at Repro.byPattern(Repro.java:23)That's JDK 21.0.12.1, running the file in the next section. The last one has no message at all, and the first one blames a variable called <local1> that exists nowhere in your source.
Picture the refactor that makes this bite. Someone swaps an if (e instanceof A) ... else if (e instanceof B) ... chain for a pattern switch, the tests stay green, and a null event that used to fall into "ignored" now kills the consumer thread. No message, a frame inside Objects, and the natural first guess is a stray requireNonNull in your own code. It isn't yours. javac put it there, and that's the trace I spent the most time on.
Feed null to every kind of switch
You need a JDK 21 or newer. I ran this on Ubuntu-packaged OpenJDK 21.0.12.1 and 25.0.4.1, with the source launcher. Save as Repro.java:
public class Repro {
enum Color { RED, GREEN }
sealed interface Shape permits Circle, Square {}
record Circle(double r) implements Shape {}
record Square(double s) implements Shape {}
static String byString(String s) {
switch (s) {
case "a": return "A";
default: return "other"; // default does not catch null
}
}
static String byEnum(Color c) {
return switch (c) { case RED -> "r"; case GREEN -> "g"; };
}
static String byBoxed(Integer i) {
switch (i) {
case 1: return "one";
default: return "other";
}
}
static String byPattern(Shape s) {
return switch (s) { // no "case null" here
case Circle c -> "circle";
case Square q -> "square";
};
}
static String byPatternSafe(Shape s) {
return switch (s) {
case null -> "no shape";
case Circle c -> "circle";
case Square q -> "square";
};
}
static String byStringSafe(String s) {
return switch (s) {
case "a" -> "A";
case null, default -> "other or null";
};
}
public static void main(String[] args) {
System.out.println(switch (args[0]) {
case "string" -> byString(null);
case "enum" -> byEnum(null);
case "boxed" -> byBoxed(null);
case "pattern" -> byPattern(null);
case "safe" -> byPatternSafe(null) + " / " + byStringSafe(null);
default -> "unknown mode";
});
}
}Run it five times:
java Repro.java string # NPE ... "String.hashCode()" ... "<local1>"
java Repro.java enum # NPE ... "Repro$Color.ordinal()" ... "<parameter1>"
java Repro.java boxed # NPE ... "java.lang.Integer.intValue()" ... "<parameter1>"
java Repro.java pattern # NPE, no message, top frame java.util.Objects.requireNonNull
java Repro.java safe # prints: no shape / other or nullEach failing one dies in well under a second with exit code 1. Nothing is shrunk here, there's no load or timing involved. The safe run is the control: same nulls, no exception.
For 8, 11 and 17 there's no pattern switch, so use this Legacy.java (only the first three cases, written with old-style switch statements):
public class Legacy {
enum Color { RED, GREEN }
static String byString(String s) {
switch (s) {
case "a": return "A";
default: return "other";
}
}
static String byEnum(Color c) {
switch (c) {
case RED: return "r";
default: return "other";
}
}
static String byBoxed(Integer i) {
switch (i) {
case 1: return "one";
default: return "other";
}
}
public static void main(String[] args) {
String mode = args[0];
if (mode.equals("string")) System.out.println(byString(null));
else if (mode.equals("enum")) System.out.println(byEnum(null));
else System.out.println(byBoxed(null));
}
}javac Legacy.java && java Legacy stringIf you see a different exception, say UnsupportedClassVersionError, you compiled with a newer javac than the JVM running it. And if the process is silent with exit code 0, you passed a mode that isn't one of the three.
Where each version stands
| 8 / 11 | 17 | 21 / 25 | |
|---|---|---|---|
| null into String / enum / boxed switch | NPE, no message | NPE with helpful message | same as 17 |
case null | doesn't compile (constant string expression required) | preview only (null in switch cases is a preview feature and is disabled by default) | final (JEP 441, 21) |
| null into pattern switch | n/a | NPE from Objects.requireNonNull, no message (needs --enable-preview) | same, no message |
default matches null | no | no | no |
The helpful-message behaviour is JEP 358, and it's the same JVM feature on every row where it appears. 8 and 11 predate it. Nothing about the rule changed between 8 and 25: null selector, no case null, NPE. What changed is whether you're allowed to write the case null, and how readable the failure is.
If you're on 8 reading NullPointerException with nothing after it, that's the version. It isn't hiding anything from you, there's nothing to print.
Short version: the selector gets dereferenced first
Before the switch picks a branch, something has to turn the selector into a number or a type. For a String that's hashCode(), for an enum it's ordinal(), for a boxed Integer it's intValue(). Call any of those on null and you get an NPE before a single case label is looked at. default is a label too, so it never gets its turn.
Pattern switches don't call a method on your object, so javac adds an explicit null check instead. That's the one with no message.
Down in the bytecode javac emits
I started where everyone starts, with the exception, and got nowhere. The trace shows my own method and a requireNonNull I never wrote. So I stopped reading the trace and disassembled the class.
javap -c -p on the string switch (javac 21):
0: aload_0
1: astore_1 // javac copies your parameter into a synthetic local
2: iconst_m1
3: istore_2 // tmp = -1
4: aload_1
5: invokevirtual String.hashCode:()I
8: lookupswitch { 97: 28 ...
28: aload_1
29: ldc "a"
31: invokevirtual String.equals:(Ljava/lang/Object;)ZThat's the whole trick. A string switch is two switches: one on hashCode() to find candidates, one on a small integer after equals confirms the match. And that first line copies your s into a compiler-made local. It's why the helpful message says <local1>, and why compiling with -g didn't help: with -g the enum and boxed cases did get real names ("c", "i"), but the string one still said <local1>, because the synthetic copy never had a name to keep. I checked that by compiling both ways. Plain javac with no -g gives you <parameter1> for the other two, which is why that's what's in the traces above.
The enum case has a quirk of its own. On javac 8 and 17, an enum switch goes through a synthetic class (Legacy$1) holding a $SwitchMap$... int array, and the bytecode reads getstatic $SwitchMap...; aload; invokevirtual ordinal; iaload. The ordinal() call is what NPEs. On javac 21, with the enum in the same file, I got no Legacy$1 at all, just a direct ordinal() call into the lookupswitch. Same exception, different shape underneath. I only checked the same-file case, so I won't claim anything about enums from another jar.
Now the pattern switch (javac 21, patternNoNull):
0: aload_0
1: dup
2: invokestatic java/util/Objects.requireNonNull:(Ljava/lang/Object;)Ljava/lang/Object;
5: pop
...
11: invokedynamic typeSwitch:(Ljava/lang/Object;I)IThere it is. Line 2. javac emits Objects.requireNonNull(selector) before the invokedynamic typeSwitch, whenever the switch has no case null. And since it's an explicit throw new NullPointerException() inside Objects, not a JVM-detected dereference, the helpful-message machinery has nothing to describe. That's the empty message.
Compile the same switch with case null and the requireNonNull is gone. Instead the typeSwitch call itself accepts null and returns -1, and a lookupswitch arm for -1 jumps to your case null body. The null check moved from "throw" to "dispatch".
JEP 441 states the rule plainly: "If the selector expression evaluates to null then any null case label is said to match. If there is no such label associated with the switch block then the switch throws NullPointerException, as before." And the part that matters for default: "To maintain backward compatibility with the current semantics of switch, the default label does not match a null selector."
I still haven't read the javac source that decides when to emit the requireNonNull versus fold null into typeSwitch. I inferred it from the bytecode of the two cases above. If you know the exact condition, I'd like to hear it.
The instanceof refactor that changes null
This is the one worth a code review comment. Same file, two methods, both "equivalent":
public class Trap {
sealed interface Event permits Created, Deleted {}
record Created(String id) implements Event {}
record Deleted(String id) implements Event {}
static String before(Event e) {
if (e instanceof Created c) return "created " + c.id();
else if (e instanceof Deleted d) return "deleted " + d.id();
return "ignored"; // null lands here
}
static String after(Event e) {
return switch (e) {
case Created c -> "created " + c.id();
case Deleted d -> "deleted " + d.id();
}; // null throws
}
public static void main(String[] a) {
System.out.println(before(null));
try { System.out.println(after(null)); }
catch (NullPointerException ex) {
System.out.println("NPE message=" + ex.getMessage() + " top=" + ex.getStackTrace()[0]);
}
}
}java Trap.java on 21 prints ignored, then NPE message=null top=java.base/java.util.Objects.requireNonNull(Objects.java:233). On 25 it's the same with line 220. instanceof is false for null, so the old chain fell through to the last line. The switch is "exhaustive", which people read as "handles everything", and it handles everything except null.
Exhaustive covers the types in the hierarchy. Null isn't one.
What to change
The fix is one line, and it forces a decision you were making by accident. The reason it works: case null makes the null check part of dispatch, so there's no requireNonNull in front of it.
return switch (e) {
+ case null -> "ignored";
case Created c -> "created " + c.id();
case Deleted d -> "deleted " + d.id();
};For switches with a default, you can fold the two together:
- default -> "other";
+ case null, default -> "other or null";That's Java 21 and up. Below that you have a short list of options, and they cost different things:
| Situation | Do this | What it costs |
|---|---|---|
| 21+ | case null (own arm, or case null, default) | nothing; null is now explicit in the code |
| 8 to 17, any switch | if (x == null) return ...; before the switch | one line per switch, easy to forget on the next one |
| 8 to 17, many callers | Objects.requireNonNull(x, "x") at the method boundary | NPE still happens, but with a message that names the argument |
| anywhere | Optional-wrap the selector | allocation and noise for a problem a null check solves |
What isn't a fix: default (never matches null, per JEP 441 and the runs above); wrapping the call in catch (NullPointerException e) (you'll also swallow genuine NPEs from inside your branches); and String.valueOf(s) to switch on "null" (it works, then someone ships a user literally named null).
Make null a decision at the edge
A switch that's allowed to see null is a switch that has a third state nobody designed. I'd rather have the boundary reject it: events come in through one place, that place calls Objects.requireNonNull(event, "event"), and everything behind it switches without case null. Then the NPE is loud, early, and named.
When null genuinely is a valid input, say so in the switch and give it its own arm. Don't let it ride default.
import java.util.Objects;
public class Better {
sealed interface Event permits Created, Deleted {}
record Created(String id) implements Event {}
record Deleted(String id) implements Event {}
static String handle(Event event) {
Objects.requireNonNull(event, "event"); // reject at the boundary
return switch (event) {
case Created c -> "created " + c.id();
case Deleted d -> "deleted " + d.id();
};
}
static String handleLenient(Event event) { // null is a real input here
return switch (event) {
case null -> "ignored";
case Created c -> "created " + c.id();
case Deleted d -> "deleted " + d.id();
};
}
public static void main(String[] args) {
System.out.println(handleLenient(null));
System.out.println(handle(new Created("42")));
try { handle(null); }
catch (NullPointerException e) { System.out.println(e.getMessage()); } // event
}
}The check that would have caught this
Tests first. Any method with a switch on a parameter gets a null case in its unit tests, and the assertion says which behaviour you meant: throws, or maps to a value. The refactor above passed review because no test had ever passed null.
In review, grep for the shape rather than the exception: a diff that deletes an instanceof chain and adds a switch deserves one question. Where does null go now?
In production, the top frame is your discriminator. java.util.Objects.requireNonNull directly under your method, with no message, points at a pattern switch without case null (or at a real requireNonNull call, which will usually carry a message). String.hashCode(), ordinal() or intValue() points at a classic switch. Group NPEs by top frame plus the first line of your own code, and these separate cleanly. I couldn't confirm that any static analyser I'd name flags a switch on a possibly-null selector, so I won't claim one does. Test it instead.
Version safety, plainly: the null-selector NPE exists on every JDK from 8 through 25 and nothing in the upgrade path removes it. Moving to 21 doesn't fix this, it only lets you write case null. Moving a chain of instanceof checks into a pattern switch on any release changes null from "falls through" to "throws".
Getting to the bottom of the pattern-switch case took me well over a day of bytecode and JEP text, mostly because the trace points at a frame you never wrote. The writeup's here so you can skip that part.
Pin this one up
- A switch dereferences its selector before it looks at any label.
defaultnever sees null. - 8 and 11: bare
NullPointerException. 17 and up: a message naminghashCode(),ordinal()orintValue(). Pattern switches: no message, top frameObjects.requireNonNull. case nullis final in 21. On 17 it's preview, below that it won't compile.- Exhaustive doesn't mean null-safe. Check every
instanceof-to-switchrefactor for it. - Compile with
-g, and don't be surprised when the string-switch message still says<local1>.
Same crash, other phrasings
Why does switch on a null string throw NullPointerException in Java?
Because javac compiles a string switch to a call to hashCode() on the selector, and calling it on null is an NPE before any label is compared. default doesn't help. On Java 21 and later you can add case null (or case null, default) to handle it explicitly.
Does the default case in a switch handle null in Java?
No. JEP 441 says the default label does not match a null selector, to keep the old semantics. A null selector throws NullPointerException unless the switch has a case null label. Combine them with case null, default if you want one arm for both.
How do I use case null in a Java switch?
On Java 21 or later, add case null -> ... as its own arm, or case null, default -> .... It works for strings, enums, boxed types and pattern switches. On 17 it needs --enable-preview, and on 8 or 11 it doesn't compile.
Why does my pattern-matching switch throw NullPointerException with no message?
javac inserts Objects.requireNonNull(selector) ahead of the typeSwitch call when there's no case null. That's an explicit throw, so the helpful NPE message has nothing to describe. Add case null, or null-check at the method boundary with a message.
Is a switch expression with all enum constants covered null-safe?
No. Exhaustive means every constant is covered, not that null is. An enum switch calls ordinal() on the selector and throws on null whether or not it has a default. Add case null on 21+, or reject null earlier.
Sources
Demand discovery was degraded: Stack Overflow tag pages were not fetched and the Stack Exchange API was unreachable from the run environment (observed 2026-10-03), so no SO view totals are cited.
- JEP 441: Pattern Matching for switch (null selector,
case null,defaultand null, quoted above) - JEP 406: Pattern Matching for switch (Preview) (first preview, JDK 17)
- JEP 358: Helpful NullPointerExceptions
java.util.Objects.requireNonNullJavadoc, JDK 21- Twelve Days of Pattern Matching (Horstmann)
- JDK-8265981, compiler implementation for pattern switch