0
0
Kotlinprogramming~15 mins

Sealed classes with when exhaustive check in Kotlin - Deep Dive

Choose your learning style9 modes available
Overview - Sealed classes with when exhaustive check
What is it?
Sealed classes in Kotlin are special classes that restrict which other classes can inherit from them. They let you define a fixed set of subclasses in one place. When you use a sealed class with a when expression, Kotlin can check if you have handled all possible cases, making your code safer and clearer.
Why it matters
Without sealed classes, you might miss handling some cases in your code, leading to bugs or crashes. Sealed classes with exhaustive when checks help catch these mistakes early, making programs more reliable. This is especially useful when dealing with different states or types that must be handled distinctly.
Where it fits
Before learning sealed classes, you should understand basic classes, inheritance, and when expressions in Kotlin. After this, you can explore advanced pattern matching, data classes, and Kotlin's type system features like inline classes or enums.
Mental Model
Core Idea
Sealed classes define a closed set of types so that when expressions can safely cover all cases without missing any.
Think of it like...
Imagine a box of colored pencils where you know exactly which colors are inside. When you pick a color to draw, you can be sure you have accounted for every color in the box.
SealedClass
├── SubclassA
├── SubclassB
└── SubclassC

when (sealedInstance) {
  is SubclassA -> ...
  is SubclassB -> ...
  is SubclassC -> ...
  // No else needed because all subclasses are covered
}
Build-Up - 7 Steps
1
FoundationUnderstanding sealed classes basics
🤔
Concept: Sealed classes restrict subclassing to a known set within the same file.
In Kotlin, declare a sealed class with the keyword 'sealed'. Only classes defined inside the same file can inherit it. This helps the compiler know all possible subclasses. Example: sealed class Result class Success(val data: String) : Result() class Error(val error: Throwable) : Result()
Result
You create a base type 'Result' with two known subclasses 'Success' and 'Error'.
Knowing that sealed classes limit subclassing helps the compiler and you to reason about all possible types in one place.
2
FoundationUsing when expressions with sealed classes
🤔
Concept: When expressions can check the type of sealed class instances and handle each subclass.
You can use 'when' to check which subclass an instance belongs to. Example: fun handle(result: Result) = when(result) { is Success -> println("Data: ${result.data}") is Error -> println("Error: ${result.error.message}") }
Result
The program prints different messages depending on the subclass of 'result'.
When expressions combined with sealed classes let you write clear and type-safe branching logic.
3
IntermediateExhaustive when checks explained
🤔Before reading on: Do you think Kotlin requires an 'else' branch when all sealed subclasses are handled in 'when'? Commit to your answer.
Concept: Kotlin knows when a when expression covers all sealed subclasses and then treats it as exhaustive, so no else branch is needed.
If you handle every subclass of a sealed class in a when expression, Kotlin considers it exhaustive and you don't need an else branch. Example: fun handle(result: Result) = when(result) { is Success -> println("Success") is Error -> println("Error") } // No else needed
Result
The compiler ensures all cases are handled, preventing missing branches.
Understanding exhaustiveness helps you write safer code without unnecessary else branches.
4
IntermediateForcing exhaustiveness with assignment
🤔Before reading on: Can assigning a when expression to a variable enforce exhaustiveness? Commit to your answer.
Concept: Assigning a when expression to a variable forces Kotlin to check that all cases are covered, making the when exhaustive.
When you assign the result of a when expression to a variable, Kotlin requires it to be exhaustive. Example: val message = when(result) { is Success -> "Got data" is Error -> "Got error" } // Compiler error if any subclass is missing
Result
The compiler forces you to handle all subclasses or add an else branch.
Knowing this trick helps catch missing cases early during compilation.
5
AdvancedSealed interfaces and exhaustive when
🤔Before reading on: Do you think sealed interfaces behave the same as sealed classes with when? Commit to your answer.
Concept: Since Kotlin 1.5, sealed interfaces can also be used with exhaustive when expressions, extending sealed class benefits to interfaces.
You can declare sealed interfaces and implement them with classes. Example: sealed interface Shape class Circle : Shape class Square : Shape fun draw(shape: Shape) = when(shape) { is Circle -> println("Circle") is Square -> println("Square") } // Exhaustive when
Result
You get the same exhaustive checking benefits with interfaces.
Understanding sealed interfaces expands your design options while keeping exhaustive safety.
6
ExpertCompiler internals of sealed when checks
🤔Before reading on: Do you think the compiler tracks subclasses at runtime or compile-time for exhaustiveness? Commit to your answer.
Concept: The Kotlin compiler tracks all subclasses of sealed classes at compile-time to verify when expressions cover all cases, without runtime overhead.
At compile-time, Kotlin knows all subclasses declared in the same file. When you write a when expression, the compiler checks if all these subclasses are handled. If yes, it marks the when as exhaustive and no else is needed. This check is static and does not add runtime cost.
Result
You get compile-time safety without slowing down your program.
Knowing this prevents confusion about performance and explains why sealed classes must be in the same file.
7
ExpertLimitations and pitfalls of sealed when checks
🤔Before reading on: Can you extend sealed classes outside their file and still get exhaustive when checks? Commit to your answer.
Concept: Sealed classes restrict subclassing to the same file, so extending them elsewhere breaks exhaustiveness and compiler checks.
If you try to subclass a sealed class outside its file, the compiler disallows it. This ensures the compiler can see all subclasses. If you use enums or open classes instead, you lose exhaustive when checks. Example: // Error: Cannot inherit from sealed class outside file class NewSubclass : Result()
Result
You must keep subclasses in the same file to keep exhaustive checks working.
Understanding this design constraint helps avoid confusing compiler errors and design mistakes.
Under the Hood
The Kotlin compiler records all subclasses of a sealed class declared in the same file during compilation. When it encounters a when expression on a sealed class, it compares the handled cases against the known subclasses. If all are covered, it marks the when as exhaustive, allowing omission of else branches. This check happens at compile-time, so no runtime cost is added. The sealed class itself is abstract and cannot be instantiated directly.
Why designed this way?
Sealed classes were designed to improve safety and clarity in type hierarchies by making all subclasses known at compile-time. This contrasts with open classes where subclasses can be anywhere, making exhaustive checks impossible. Restricting subclasses to the same file simplifies compiler analysis and prevents accidental missing cases. Alternatives like enums are more limited, while sealed classes allow richer data and behavior per subclass.
SealedClass (abstract)
├── Subclass1
├── Subclass2
└── Subclass3

Compiler checks when expression:

when (instance) {
  is Subclass1 -> ...
  is Subclass2 -> ...
  is Subclass3 -> ...
} -> Exhaustive

If any subclass missing -> Compiler error
Myth Busters - 4 Common Misconceptions
Quick: Does Kotlin require an else branch in when if all sealed subclasses are handled? Commit yes or no.
Common Belief:You always need an else branch in when expressions to cover all cases.
Tap to reveal reality
Reality:If you handle all subclasses of a sealed class, Kotlin does not require an else branch because it knows the when is exhaustive.
Why it matters:Adding unnecessary else branches can hide missing cases and reduce code clarity.
Quick: Can you subclass a sealed class anywhere in your project? Commit yes or no.
Common Belief:Sealed classes can be subclassed anywhere like open classes.
Tap to reveal reality
Reality:Sealed classes can only be subclassed in the same file where they are declared.
Why it matters:Trying to subclass sealed classes elsewhere causes compiler errors and breaks exhaustive when checks.
Quick: Does the sealed class exhaustiveness check add runtime overhead? Commit yes or no.
Common Belief:Exhaustive when checks add runtime performance cost.
Tap to reveal reality
Reality:Exhaustiveness is checked at compile-time only, so there is no runtime overhead.
Why it matters:Misunderstanding this might lead to avoiding sealed classes unnecessarily.
Quick: Are sealed interfaces treated differently than sealed classes for when checks? Commit yes or no.
Common Belief:Sealed interfaces do not support exhaustive when checks like sealed classes.
Tap to reveal reality
Reality:Since Kotlin 1.5, sealed interfaces also support exhaustive when checks just like sealed classes.
Why it matters:Not knowing this limits design choices and code safety.
Expert Zone
1
Sealed classes require all subclasses in the same file, but those subclasses can be nested or top-level, allowing flexible organization.
2
When expressions on sealed classes can be used as expressions (returning values) or statements, and exhaustiveness applies in both cases.
3
Combining sealed classes with data classes for subclasses allows rich, immutable data structures with exhaustive pattern matching.
When NOT to use
Avoid sealed classes when your type hierarchy needs to be extended across multiple files or modules. In such cases, consider using open classes or interfaces without sealed restrictions. Also, if you need runtime polymorphism with unknown subclasses, sealed classes are not suitable.
Production Patterns
In production, sealed classes are widely used to represent UI states, network responses, or domain events where all possible cases are known. Exhaustive when expressions ensure all states are handled, preventing crashes. They also simplify maintenance by making missing cases compile errors rather than runtime bugs.
Connections
Algebraic Data Types (ADTs)
Sealed classes implement the concept of sum types in ADTs, representing a fixed set of alternatives.
Understanding sealed classes as ADTs helps grasp their power in modeling complex data with safety and exhaustiveness.
Pattern Matching in Functional Programming
Exhaustive when expressions on sealed classes are Kotlin's version of pattern matching found in languages like Haskell or Scala.
Knowing this connection reveals how Kotlin blends object-oriented and functional styles for safer code.
Finite State Machines (FSM)
Sealed classes can model FSM states, and exhaustive when expressions ensure all states are handled.
This connection shows how sealed classes help implement reliable state-driven logic in software.
Common Pitfalls
#1Missing a subclass in when expression without else branch.
Wrong approach:fun handle(result: Result) = when(result) { is Success -> println("Success") // Missing Error case }
Correct approach:fun handle(result: Result) = when(result) { is Success -> println("Success") is Error -> println("Error") }
Root cause:Not realizing that when expressions must cover all sealed subclasses or include else to be exhaustive.
#2Trying to subclass a sealed class outside its file.
Wrong approach:class NewResult : Result() // in a different file
Correct approach:// Define subclass inside the same file as sealed class class NewResult : Result()
Root cause:Misunderstanding sealed class restriction that all subclasses must be in the same file.
#3Adding unnecessary else branch when all subclasses are handled.
Wrong approach:when(result) { is Success -> ... is Error -> ... else -> ... // unnecessary }
Correct approach:when(result) { is Success -> ... is Error -> ... }
Root cause:Not knowing Kotlin's compiler can detect exhaustiveness for sealed classes and allow omitting else.
Key Takeaways
Sealed classes let you define a fixed set of subclasses known at compile-time.
When expressions on sealed classes can be exhaustive, so you don't need else branches if all cases are handled.
Exhaustiveness is checked by the compiler at compile-time, preventing runtime errors from missing cases.
Sealed classes must have all subclasses declared in the same file to enable exhaustive when checks.
Sealed interfaces extend this concept, allowing more flexible designs with the same safety benefits.