You need a value from a handful of cases. Classic switch makes you declare a mutable local, break every arm, and hope nobody deletes a break. Miss one and the next case silently runs — the oldest Java footgun that still ships in production.
A switch expression returns a value. Arrow cases do not fall through. The compiler demands that every possible selector is covered. Reach for this when the switch is the result — a mapped code, a label, a computed amount — not when you are only firing side effects and moving on.
Pattern matching for switch (type patterns, record deconstruction, when guards) is a later story. This post is JEP 361 only: expression switch, ->, yield, and why break no longer produces the value.
The problem: a result plus a ritual
The pre-14 way to turn a day into a letter count looks like this:
DayOfWeek day = DayOfWeek.WEDNESDAY;
int letters;
switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
letters = 6;
break;
case TUESDAY:
letters = 7;
break;
case THURSDAY:
case SATURDAY:
letters = 8;
break;
case WEDNESDAY:
letters = 9;
break;
default:
throw new IllegalStateException("unexpected: " + day);
}
Three jobs are tangled: initialize a variable the compiler cannot treat as definitely assigned without default or every enum constant, remember break, and keep the mapping in your head. Drop the break after TUESDAY and Wednesday’s 9 overwrites 7. The compiler is fine with that. Your tests may not be.
The same mapping as an expression:
int letters = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> 9;
};
No mutable local. No break. Comma-separated labels share an arm. If you add a constant to your enum later, this switch stops compiling until you handle it.
When switch expressions shipped
| Release | Status | Spec |
|---|---|---|
| Java 12 | First preview (break with a value) | JEP 325 |
| Java 13 | Second preview (yield replaces break value) | JEP 354 |
| Java 14 | Standard feature | JEP 361 |
Use Java 14+. No --enable-preview. If a snippet still says break 6;, that is the Java 12 preview — it is not how the final feature works.
Mental model: statement vs expression
A switch statement is control flow. It runs arms for their side effects. Completing without a value is fine. A switch expression is a value. You assign it, return it, or pass it. Completing without a result is a compile error.
| Statement | Expression | |
|---|---|---|
| Role | Do work | Produce a value |
| Completes with | Nothing (or you return / throw yourself) | A result, or a throw |
| Exhaustive? | No — missing cases are silent | Yes — every possible selector |
Colon (:) labels | Classic fall-through | Group labels, then yield |
Arrow (->) labels | Allowed; no fall-through | Allowed; no fall-through |
Both forms still exist. JEP 361 did not delete traditional switch. It added an expression form and arrow labels you can use in either.
Note: You cannot mix case L: and case L -> in the same switch. Pick one label style per switch.
Arrow vs colon
The arrow is the part that kills the ritual. The body is one of three things: an expression, a block, or a throw. Adjacent arrows never run into each other. Grouping is the comma list on the label, not a missing break.
String mood = switch (day) {
case SATURDAY, SUNDAY -> "weekend";
case MONDAY -> "start";
default -> "weekday";
};
Arrow labels also work on a statement when you want the no-fall-through rule without producing a value:
switch (day) {
case SATURDAY, SUNDAY -> System.out.println("off");
case MONDAY -> System.out.println("stand-up at 9");
default -> System.out.println("ship it");
}
That is still a statement — no assignment on the left — but it is no longer the fall-through minefield.
Colon cases remain for the classic grouping style. In an expression, you do not break a value out. You yield it:
int letters = switch (day) {
case MONDAY, FRIDAY, SUNDAY:
yield 6;
case TUESDAY:
yield 7;
case THURSDAY, SATURDAY:
yield 8;
case WEDNESDAY:
yield 9;
};
Prefer arrows for new code. Reach for colon + yield only when you are converting a large fall-through statement and want a smaller diff.
yield in a block
When an arm needs more than a single expression — locals, a short calculation, a log line — wrap it in a block and yield the result:
int discount = switch (tier) {
case GOLD -> 20;
case SILVER -> 10;
case BRONZE -> {
int base = 5;
if (orderCount > 10) {
base += 2;
}
yield base;
}
};
The block is scoped to that arm. Variables declared inside it do not leak. The arm must yield (or throw); falling off the end of the block is a compile error.
Do not use break to produce a value — that was the preview spelling in Java 12, and it is not valid in the standard feature. yield completes the enclosing switch expression. break is the old statement jump.
throw is a legal arrow body on its own, which is handy once the real arms are covered:
int httpFamily = switch (status / 100) {
case 2 -> 200;
case 4 -> 400;
case 5 -> 500;
default -> throw new IllegalArgumentException("unsupported: " + status);
};
Exhaustiveness
A switch expression must handle every value the selector can have. That is the compiler’s replacement for “forgot a default and left the local uninitialized.”
For an enum, list every constant, or provide default. Add a constant later and every exhaustive expression switch without default becomes a compile error — that is the feature, not a nuisance.
enum Tier { GOLD, SILVER, BRONZE }
static int rate(Tier tier) {
return switch (tier) {
case GOLD -> 20;
case SILVER -> 10;
case BRONZE -> 5;
};
}
No default. tier cannot be anything else unless you change Tier.
For int, String, and other open types, the value set is not closed, so you need default:
static String parity(int n) {
return switch (n) {
case 0 -> "zero";
default -> (n % 2 == 0) ? "even" : "odd";
};
}
A switch statement is still allowed to be incomplete. That is why the old bugs survived so long: the language treated missing cases as “do nothing.” If you keep the statement form, you opted out of that alarm.
A default on a fully covered enum compiles even after you add a constant — the new constant silently hits default. Prefer listing every constant when the set is the domain.
Note: A null selector still throws NullPointerException here, same as classic switch. case null arrives with pattern matching for switch, not with JEP 361.
Traditional switch statements still exist
Nothing forces you to rewrite every switch. Side-effect-only dispatch — start a job, set several fields, break out of a surrounding loop — can stay a statement.
switch (event) {
case "start":
running = true;
startedAt = Instant.now();
break;
case "stop":
running = false;
break;
default:
log.warn("ignored event {}", event);
}
What you should stop doing is using a statement plus a mutable local plus break just to compute one value. That is the expression’s job.
Use cases
1. Map a closed enum to a value
Status codes, billing tiers, and HTTP methods are the everyday fit:
enum Method { GET, HEAD, POST, PUT, PATCH, DELETE }
static boolean isSafe(Method method) {
return switch (method) {
case GET, HEAD -> true;
case POST, PUT, PATCH, DELETE -> false;
};
}
2. Return from a method in one expression
Keep the mapping next to return instead of a local that exists only to satisfy definite assignment:
static String label(DayOfWeek day) {
return switch (day) {
case MONDAY -> "plan";
case FRIDAY -> "ship";
case SATURDAY, SUNDAY -> "off";
default -> "build";
};
}
3. Nested computation without a temporary
A switch expression is an expression. It can sit in a constructor call, a stream map, or another expression:
Money fee = new Money(switch (plan) {
case FREE -> 0;
case STARTER -> 9;
case PRO -> 29;
});
Pattern switch is later
Arrow cases still match on constants (enums, strings, integral values), not on types. Sealed hierarchies, record patterns, and when guards are the Java 21 half of the story — exhaustive type switch, not exhaustive constant switch. That lives in Sealed Classes + Pattern Switch. Read that post when the selector is a type in a closed family, not a DayOfWeek.
Gotchas
Mixing label styles
case A -> and case B: in one switch is a compile error. Convert the whole switch or leave it.
break is not a result
In an expression, break does not yield a value. Use an arrow expression, or yield inside a block. Do not return from inside the expression to “finish” an arm either — produce the switch’s value, then return switch (...) { ... } if that is the method result.
Statements stay non-exhaustive
Rewriting only the arrows but leaving a statement (no assignment) does not buy exhaustiveness. The alarm is tied to the expression form.
yield as a name
yield is a restricted identifier used as the yield statement. Existing calls such as Thread.yield() still compile. A bare yield 2; is the switch statement, not a method call.
Cheat sheet
int v = switch (x) {
case A, B -> 1; // arrow: no fall-through
case C -> { yield 2; } // block: yield the value
default -> 0; // required unless enum is fully listed
};
switch (x) { // statement still valid
case A, B -> doWork();
default -> log(x);
}
Java 14+ (JEP 361); previewed in 12/13
Arrow: no fall-through. Colon + yield: grouping, then a value.
Expression switch: exhaustive. Statement switch: not.
Do not break a value (Java 12 preview). Use yield.
Pattern switch / sealed types: later post (Java 21).
Do:
- Use an expression switch when the
switchis the result. - List every enum constant so a new one breaks the build.
- Use comma-separated labels instead of stacked colon cases.
Don’t:
- Rely on missing
breakto share logic — that is the old bug. - Use
breakto produce a value in an expression. - Expect type patterns or
whenhere — that is pattern switch, not JEP 361.
Wrap-up
Switch expressions make switch a value: Java 14 / JEP 361, after previews in 12 and 13. Arrow cases remove accidental fall-through. yield is how a block produces that value. The compiler insists the expression is exhaustive, which is why enum switches without default become a build break when the enum grows.
Keep traditional statements for side-effect dispatch. When you need to match on types in a sealed family, that is pattern switch, not this feature.