Switch Expressions

Switch expressions (Java 14+) let switch produce a value directly with concise arrow syntax, eliminating fall-through bugs and forcing exhaustive handling.

A switch expression (standardized in Java 14, JEP 361) evaluates to a value, unlike the traditional switch *statement*, which only executes code. Paired with the arrow (->) case syntax, it removes the two classic switch pain points: accidental fall-through from a missing break, and verbose temporary-variable assignment patterns.

Statement vs Expression

A traditional switch statement executes code as a side effect and produces no value itself — if you want a result, you assign to a variable inside each case. A switch expression IS a value-producing expression: switch (...) { ... } itself evaluates to a value you can assign, return, or pass as an argument.

// Old style: switch statement, verbose result assembly
String oldStyle;
switch (day) {
    case 6:
    case 7:
        oldStyle = "Weekend";
        break;
    default:
        oldStyle = "Weekday";
}

// New style: switch expression
String newStyle = switch (day) {
    case 6, 7 -> "Weekend";
    default   -> "Weekday";
};

Arrow Labels (->)

Arrow-form case labels (case X ->) execute only the code immediately after the arrow — a single expression, a block, or a throw — and never fall through to the next case, regardless of what happens. Multiple constants can share one arrow label, comma-separated, replacing the old grouped-fall-through-labels idiom.

int day = 3;
String name = switch (day) {
    case 1, 7 -> "Weekend-ish"; // multiple labels, one arm
    case 2, 3, 4, 5, 6 -> "Weekday";
    default -> "Invalid";
};
System.out.println(name); // Weekday

yield — Returning a Value From a Block Arm

When a case arm needs multiple statements (not just one expression), use a block { ... } and end it with yield value; to produce that arm's result. yield is the expression-switch analog of return for a method — it exits the switch expression with a value, not the enclosing method.

int quantity = 7;
String tier = switch (quantity) {
    case 1, 2, 3 -> "small";
    default -> {
        String label = quantity < 10 ? "medium" : "large";
        yield label; // required inside a block arm
    }
};
System.out.println(tier); // medium

Traditional Colon Labels Can Also Use yield

Old-style colon (:) case labels can also be used inside a switch EXPRESSION (not just statements), but then every arm must explicitly yield its value (or throw), and fall-through between colon-labeled cases still applies exactly as in a traditional switch statement — this mixing is legal but rarely preferred over arrow form.

String result = switch (day) {
    case 6:
    case 7:
        yield "Weekend";
    default:
        yield "Weekday";
};

Fall-Through Differences

This is the core behavioral difference from traditional switch: arrow-form labels never fall through — each matched arm executes exactly its own code, full stop. Colon-form labels (in both switch statements and colon-style switch expressions) fall through to the next label unless explicitly stopped by break, yield, return, throw, or continue.

Arrow (->) labels        : never fall through, no break needed
Colon (:) labels          : fall through unless break/yield/return/throw stops it

Exhaustiveness Requirement

A switch expression must be exhaustive — it must produce a value for every possible input, since the expression must always evaluate to something. For most types this means a default branch is mandatory; the compiler rejects a switch expression that omits one and doesn't otherwise prove full coverage (see the Pattern Matching for switch topic for how sealed types and enums can achieve exhaustiveness without an explicit default).

// compile-time error: switch expression does not cover all possible input values
// String result = switch (n) {
//     case 1 -> "one";
//     case 2 -> "two";
// };

String result = switch (n) {
    case 1 -> "one";
    case 2 -> "two";
    default -> "other";
};

Exhaustiveness With enum

A switch expression over an enum type can be exhaustive without a default if every enum constant has its own case — the compiler knows an enum's complete, fixed set of constants and can verify full coverage. This has the useful side effect that adding a new enum constant later will cause the compiler to flag every switch over that enum that doesn't yet handle it, if it has no default catch-all.

enum Level { LOW, MEDIUM, HIGH }

static int weight(Level l) {
    return switch (l) {
        case LOW -> 1;
        case MEDIUM -> 2;
        case HIGH -> 3;
        // no default needed - all 3 constants covered
    };
}

Switch Expressions Can throw

An arm of a switch expression can throw instead of producing a value — this is legal because throwing transfers control away entirely, so it doesn't violate the 'every arm must yield a value' rule. This is a clean way to reject invalid inputs inline.

static int daysInMonth(int month) {
    return switch (month) {
        case 1, 3, 5, 7, 8, 10, 12 -> 31;
        case 4, 6, 9, 11 -> 30;
        case 2 -> 28;
        default -> throw new IllegalArgumentException("Invalid month: " + month);
    };
}

No Implicit Fall-Through Bugs With Arrow Form

The classic pre-Java-14 bug — forgetting a break and silently falling into the next case — is structurally impossible with arrow-form labels, since each arm is isolated by design. This is one of the biggest practical wins of switch expressions even outside pure value-producing use cases; you can use arrow-form switch as a statement too, purely for this safety, without needing a resulting value.

// Arrow-form switch used as a statement (no assigned value) - still fall-through-safe
switch (status) {
    case OK -> System.out.println("All good");
    case WARN -> System.out.println("Check logs");
    case FAIL -> System.out.println("Escalate!");
}

Key Takeaways

A switch expression produces a value; a switch statement only executes code as a side effect.

Arrow-form (->) labels never fall through, replacing the old break-per-case pattern.

yield exits a block arm with a value, analogous to return exiting a method.

Colon-form labels can still be used in a switch expression, but then fall-through applies and every arm needs an explicit yield.

A switch expression must be exhaustive; enums and sealed types can achieve this without an explicit default.

int result = switch (n) { case 1: yield 10; // missing break-equivalent between colon cases case 2: yield 20; };

int result = switch (n) { case 1 -> 10; case 2 -> 20; default -> throw new IllegalArgumentException(); };

Colon-form case labels inside a switch expression still fall through unless yield/break stops each one individually, and this example is also non-exhaustive (missing default); arrow form avoids both problems.

String s = switch (day) { case 1 -> "Mon"; case 2 -> "Tue"; }; // compile error: not exhaustive

String s = switch (day) { case 1 -> "Mon"; case 2 -> "Tue"; default -> "Other"; };

A switch expression must produce a value for every possible input; an int selector has far more possible values than the two handled cases, so a default (or full coverage) is required.

String tier = switch (n) { default -> { String label = compute(n); label; // missing yield - compile error } };

String tier = switch (n) { default -> { String label = compute(n); yield label; } };

A block arm must explicitly yield its result value; simply evaluating an expression as the last statement of the block (as you might in a lambda body) does not implicitly become the arm's value.

What is the fundamental difference between a switch statement and a switch expression?

A switch statement executes code and produces no value of its own — any result must be assigned inside each case as a side effect. A switch expression evaluates to a value directly, so the whole switch(...) construct can be assigned, returned, or passed as an argument.

Why do arrow-form (->) case labels never fall through?

By design, each arrow-labeled arm's code is isolated — once its expression, block, or throw executes, control exits the switch entirely. There's no need for (or possibility of) fall-through into the next label, unlike colon-form labels.

What does yield do inside a switch expression's block arm, and how is it different from return?

yield supplies the value that the enclosing switch expression evaluates to for that arm, and then exits just the switch expression. return, by contrast, exits the entire enclosing method — using return inside a switch expression's arm would be a compile-time error (or exit the method instead of the switch, depending on context) if the switch isn't itself the whole method body.

Why must a switch expression be exhaustive?

Because it's an expression that must evaluate to some value on every possible path — if some input weren't handled, the switch expression would have nothing to produce, which the language doesn't allow. This is why a default (or provably complete coverage, e.g. all enum constants or all permitted sealed subtypes) is required.

Can a switch expression's arm throw an exception instead of producing a value? Why is that allowed?

Yes. Throwing transfers control away from the switch expression entirely rather than completing it, so it doesn't violate the requirement that every arm produce a value — there's no value needed if that path never returns to the switch's result at all.

Can arrow-form switch be used as a statement (not assigned to anything)?

Yes — arrow-form switch works as either a statement or an expression. Used as a statement, it still gets the fall-through-safety benefit (no missing-break bugs) even without producing or using a value.

How can a switch expression over an enum be exhaustive without a default case?

The compiler knows an enum type's complete, fixed set of constants at compile time. If every constant has its own case in the switch, the compiler can prove every possible input is covered without needing a catch-all default.

Write a switch expression that converts a numeric month (1-12) to its season name ("Winter", "Spring", "Summer", "Fall"), using multiple labels per arm.

static String season(int month) { return switch (month) { case 12, 1, 2 -> "Winter"; case 3, 4, 5 -> "Spring"; case 6, 7, 8 -> "Summer"; case 9, 10, 11 -> "Fall"; default -> throw new IllegalArgumentException("Invalid month: " + month); }; } System.out.println(season(4)); // Spring

Spring — month 4 matches the case 3, 4, 5 arrow label; invalid months (e.g. 0, 13) fall to default and throw.

What does this traditional colon-form switch statement print, and why?

three — with colon labels, execution falls through from case 2 into case 3 (no break stops it), overwriting result each time, until the break in case 3 stops further fall-through. This is exactly the bug class that arrow-form switch expressions eliminate.

Write a switch expression with a block arm using yield that returns "even and large" or "even and small" for even numbers (threshold 100), and "odd" otherwise.

static String classify(int n) { return switch (n % 2) { case 0 -> { String size = n >= 100 ? "large": "small"; yield "even and " + size; } default -> "odd"; }; } System.out.println(classify(150)); // even and large System.out.println(classify(7)); // odd even and large, then odd — the block arm for n % 2 == 0 computes a local variable and yields the combined string; odd numbers fall to default.

Does this compile? Why or why not?

No, it fails to compile: the switch expression is not exhaustive. Status.WARN is a valid enum constant with no matching case and no default, so the compiler can prove a possible input (WARN) produces no value.

int n = 2;
String result;
switch (n) {
    case 1:
        result = "one";
    case 2:
        result = "two";
    case 3:
        result = "three";
        break;
    default:
        result = "other";
}
System.out.println(result);
enum Status { OK, WARN, FAIL }
int code = switch (Status.WARN) {
    case OK -> 0;
    case FAIL -> 2;
};