Why might you deliberately avoid writing a default arm in a switch over a sealed type, even though default would make the switch compile?
answer
- default = compiler can never see incomplete coverage
- no default => new subtype breaks the build (good)
- compiler enumerates every consumer to fix
- keep default only for a real catch-all semantic
- hidden MatchException backstop still exists
basics
~20 sLeaving out default makes the compiler force you to handle every subtype. If you add a new subtype later, the switch stops compiling and shows you exactly where to update it. A default would hide that.
solid answer
~50 sOmitting default turns the compiler into a change detector. With a sealed type, an exhaustive switch with no default compiles only when every permitted subtype is covered. The moment you extend the hierarchy with a new permitted subtype, every such switch that doesn't handle it fails to compile, giving you a precise, complete list of places to fix. A catch-all default would absorb the new subtype silently, so the code still compiles but does the wrong thing — a classic latent bug that surfaces only at runtime, possibly in production. The trade-off: you lose the convenience of a single fallback, and you must consciously handle each case. For closed domain models (state machines, AST nodes, command types) this compile-time exhaustiveness is exactly the safety you want, so default is intentionally avoided unless there is a genuine, semantically meaningful 'everything else' case.
go deeper
Understands that no default forces handling each subtype and that adding one later breaks the build.
Explains that default makes the switch trivially exhaustive and thus hides missing cases, while omitting it surfaces them at compile time.
Frames it as a deliberate trade-off, knows when a default is still appropriate, and ties it to closed domain modeling.
Connects to algebraic-data-type/exhaustive-match discipline, the MatchException backstop, binary-compatibility nuances, and team-scale refactoring safety.
## Setup A **sealed type** restricts who can extend it via a `permits` clause, e.g. `sealed interface Event permits Created, Updated, Deleted {}`. The full subtype set is therefore fixed and known to the compiler. A **`switch` expression** must be **exhaustive** (cover every possible input). For a sealed selector, exhaustiveness can be achieved two ways: (a) one `case` per permitted subtype, or (b) a `default` arm catching everything not otherwise listed. ## The two strategies and what they cost **With `default`:** the switch is trivially exhaustive no matter what, because `default` catches the rest. Convenient, but it means the compiler can no longer tell you when coverage becomes incomplete — there is no such thing as 'incomplete' once a catch-all exists. **Without `default` (one case per subtype):** the switch compiles *only if* it covers every permitted subtype. This couples the switch to the hierarchy at compile time. ## The payoff: the compiler as a refactoring assistant Imagine the team adds `record Archived(...) implements Event {}` and updates `permits`. Recompiling the project now produces an error at *every* default-less switch that doesn't yet handle `Archived`: > the switch expression does not cover all possible input values This is gold for large codebases: instead of hoping reviewers spot every place that processes an `Event`, the build mechanically enumerates them. You fix each, and only when all are handled does the project compile again. This is the same discipline functional languages get from exhaustive pattern matching on sum types — sealed types bring it to Java. Had those switches contained `default -> throw new IllegalStateException()` or `default -> handleGenerically(e)`, they would compile unchanged and the new `Archived` events would be mishandled silently. The bug then escapes the compiler, escapes tests that don't exercise `Archived`, and reaches runtime. ## When a `default` IS appropriate Not every switch should drop default. Use `default` when there genuinely is a meaningful 'all other cases behave the same' branch — for example, mapping many subtypes to one fallback rendering, or when you intentionally do *not* want to be forced to revisit the switch for new subtypes (a stable, open-ended fallback). The rule of thumb: omit `default` when each case needs *specific* handling and you want the compiler to enforce completeness; keep `default` when there's a real, intentional catch-all semantic. ## Subtle backstop: `MatchException` When you omit `default`, the compiler still inserts a hidden default that throws `java.lang.MatchException`. This only fires if, at runtime, a subtype appears that wasn't known when this switch was compiled — e.g. the sealed type was recompiled with a new subtype but this consumer class was not. It's a binary-compatibility safety net, not something you handle in source. So 'no default' in source does not mean 'no fallback at all' at the bytecode level; it means 'no source-level catch-all that defeats exhaustiveness checking.' ## Summary Avoiding `default` is a deliberate design choice that converts 'we might forget a case' from a runtime risk into a compile-time error. It is most valuable for closed domain models where every subtype demands distinct handling.
- Is there ever a legitimate reason to keep a default arm on a sealed switch?Yes — when there's a genuine, intended 'everything else behaves the same' branch, or when you deliberately want a stable fallback and do NOT want to be forced to revisit the switch as the hierarchy grows.
- If I omit default, is there truly no fallback in the compiled code?At source level there's no catch-all, but the compiler inserts a synthetic default that throws MatchException to handle a subtype that appears at runtime but was unknown at compile time (a separate-compilation/binary-incompatibility scenario).
saying these in an interview costs you the question
- Treating default as always the safe choice.
- Believing 'no default' means the switch has no runtime fallback at all (MatchException exists).
- Adding default 'just in case' on a closed domain model, defeating exhaustiveness.
- Assuming tests will catch a missed subtype that default silently absorbs.