> ## Content Index
> Fetch the complete content index at: https://www.ggorantala.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# NullPointerException in switch: null selector and case null
- URL: https://www.ggorantala.dev/java-switch-nullpointerexception-null-selector-case-null/
- Published: 2026-10-03T07:31:38.000Z
- Updated: 2026-10-03T07:31:54.000Z
- Description: A switch on a null String, enum, Integer or sealed type throws NullPointerException, and default won't catch it. Why, how 8 to 25 differ, and the fix.
- Author: Gopi Gorantala
- Tags: Java, java-errors, nullpointerexception, core-java, jvm, java-17, java-records

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:

```log
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`:

```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:

```bash
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 null
```

Each 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):

```java
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));
    }
}
```

```bash
javac Legacy.java && java Legacy string
```

If 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):

```log
 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;)Z
```

That'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`):

```log
 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)I
```

There 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":

```java
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.

```diff
 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:

```diff
-    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`.

```java
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. `default` never sees null.
- 8 and 11: bare `NullPointerException`. 17 and up: a message naming `hashCode()`, `ordinal()` or `intValue()`. Pattern switches: no message, top frame `Objects.requireNonNull`.
- `case null` is final in 21\. On 17 it's preview, below that it won't compile.
- Exhaustive doesn't mean null-safe. Check every `instanceof`\-to-`switch` refactor 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](https://openjdk.org/jeps/441) (null selector, `case null`, `default` and null, quoted above)
- [JEP 406: Pattern Matching for switch (Preview)](https://openjdk.org/jeps/406) (first preview, JDK 17)
- [JEP 358: Helpful NullPointerExceptions](https://openjdk.org/jeps/358)
- [java.util.Objects.requireNonNull Javadoc, JDK 21](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Objects.html)
- [Twelve Days of Pattern Matching (Horstmann)](https://horstmann.com/unblog/2023-12-01/index.html)
- [JDK-8265981, compiler implementation for pattern switch](https://bugs.openjdk.org/browse/JDK-8265981)