JAVA / BRANCHING AND SWITCH EXPRESSIONS
yield, exhaustiveness and the default branch
Use yield to produce a value from a braced switch case, and decide when a default branch is required instead of full enum coverage.
What you will learn
- Return a value from a block-bodied case with yield, never with return.
- Make every path of a block case end in yield or throw.
- Cover all enum constants instead of adding default so new ones break the build.
- Add default for int, String and any selector the compiler cannot enumerate.
Understanding yield, exhaustiveness and the default branch
An arrow case whose body is a single expression already produces a value: case LOW -> 100 makes 100 the value of the whole switch. The moment a case needs braces, for a local variable or an if, its body is a block, and blocks evaluate to nothing, so every path through that block has to end in yield value. yield is not return: it delivers a value to the switch and leaves the switch, whereas return would abandon the enclosing method, which is why Java rejects return inside a switch expression.
Exhaustiveness follows from the same fact. A switch expression sits where a value is required, so the compiler has to prove that no selector value can slip through unhandled; there is no valueless state in Java's type system to fall back on. When the selector is an int or a String the value set is far too large to list, so default is the only practical proof; when it is an enum the compiler knows every constant, and naming them all is proof enough. A switch statement carries no such duty, because doing nothing is a legal outcome for a statement.
That makes default a design decision rather than boilerplate. It is mandatory for open-ended selectors and optional for a fully covered enum, and adding it to a covered enum switch throws away real help: if a colleague adds a constant, a switch without default stops compiling at the one place that needs updating, while a switch with default silently routes the new constant to the fallback. Reach for default when a catch-all genuinely means something, such as an unrecognised input string, and leave it out when every case must be considered deliberately.
Both syntactic forms can yield. In the colon form a group may run several statements and then finish with yield, which also ends the case, so the fall-through you get in a switch statement does not apply to values in a switch expression.
public class YieldDemo {
enum Level { LOW, MEDIUM, HIGH }
static int budget(Level level, int staff) {
return switch (level) {
case LOW -> 100;
case MEDIUM -> 100 + 50 * staff;
case HIGH -> {
int base = 100 + 50 * staff;
int surge = staff > 3 ? 200 : 75;
yield base + surge;
}
};
}
static String tier(int code) {
return switch (code) {
case 1, 2 -> "basic";
case 3 -> "plus";
default -> "unknown(" + code + ")";
};
}
public static void main(String[] args) {
for (Level l : Level.values()) {
System.out.println(l + " -> " + budget(l, 5));
}
System.out.println("HIGH with 2 staff: " + budget(Level.HIGH, 2));
System.out.println(tier(3) + " / " + tier(9));
}
}A switch expression must produce exactly one value on every path, so yield supplies that value from a block and exhaustiveness, by full coverage or default, proves no input can escape without one.
Worked examples
yield in the colon form
A switch expression written with colon labels, where yield both supplies the value and ends the case.
public class ColonYield {
static int weight(String kind) {
return switch (kind) {
case "gold":
int bonus = 5;
yield 10 + bonus;
case "silver":
yield 10;
default:
System.out.println("unrated: " + kind);
yield 0;
};
}
public static void main(String[] args) {
System.out.println(weight("gold"));
System.out.println(weight("silver"));
System.out.println(weight("bronze"));
}
}Example explained
Line 1yield 10 + bonus; supplies the value for the gold group and leaves the switch, so control never falls through into case "silver".
Line 2The colon form lets a group run several statements first, which is why int bonus = 5; may precede the yield.
Line 3default is required here because a String selector has far more possible values than the two labels listed.
Line 4The line printed inside default appears before the 0 that main prints, because the switch has to finish before the caller can print its value.
yield or throw, but never nothing
A block case that yields on one path and throws on the other, in an enum switch that needs no default.
public class ModeGate {
enum Mode { READ, WRITE }
static String open(Mode m, boolean locked) {
return switch (m) {
case READ -> "read-only handle";
case WRITE -> {
if (locked) {
throw new IllegalStateException("locked for writing");
}
yield "writable handle";
}
};
}
public static void main(String[] args) {
System.out.println(open(Mode.READ, true));
System.out.println(open(Mode.WRITE, false));
try {
open(Mode.WRITE, true);
} catch (IllegalStateException e) {
System.out.println("caught: " + e.getMessage());
}
}
}Example explained
Line 1case READ -> "read-only handle"; needs no yield, since an arrow case with a single expression yields it implicitly.
Line 2The WRITE block yields on one path and throws on the other; throwing satisfies the compiler because a thrown exception never completes normally, so no value is owed.
Line 3Both constants of Mode are listed, so default is unnecessary, and adding a third constant would break this method at compile time.
Line 4The catch in main shows the only legal way out of a switch expression without a value: an exception, not a missing yield.
Important notes
default does not catch null. Switching on a null String or enum reference throws NullPointerException before any label is examined, so check for null before the switch.
An exhaustive enum switch expression is never left without a value at runtime: javac inserts a hidden default that throws an error if the code meets an enum constant that did not exist when it was compiled.
Common mistakes
Writing return inside a switch expression block instead of yield: it does not compile, because the value is needed at the switch's own position and the switch cannot exit the method on your behalf.
Ending a block case with an if that yields on only one path: the compiler rejects it because the block can finish without producing a value, so you never get a silent 0 or null.
Bolting default -> throw new IllegalStateException(...) onto a switch that already covers every enum constant: the build keeps passing when a constant is added later, and the failure moves from compile time to runtime.
Try it yourself
Change, predict, then run
Write int fee(Plan plan, int months) over enum Plan { FREE, BASIC, PRO } so that FREE gives 0, BASIC gives 9 * months, and PRO uses a braced case that yields 15 * months minus a 20 discount when months is at least 12. Then delete the BASIC case, read the compiler error, and put it back.
Open the Java workspaceCheck your understanding
An enum Status has the constants NEW and DONE, and a switch expression over it lists both with arrow cases and no default. A colleague adds CANCELLED to the enum and rebuilds. What happens to that switch expression?
- It stops compiling until CANCELLED is handled, because listing every constant was the only thing making it exhaustive.
- It compiles and produces null for CANCELLED, since no branch matches.
- It compiles and produces the value of the first case for CANCELLED.
- It compiles, and CANCELLED falls through to the last case listed.
Show answer
Complete coverage of the constants was the proof of exhaustiveness, so a new constant destroys that proof and javac reports that the expression does not cover all possible input values. Producing null is the tempting answer but impossible: a switch expression has no valueless outcome, and an int-valued switch could not return null at all. The only way to get a silent fallback is to write default yourself, which is exactly why leaving it out is the safer design.