Bird
Raised Fist0
Kotlinprogramming~15 mins

Sealed classes with when exhaustive check in Kotlin - Deep Dive

Choose your learning style10 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.

Practice

(1/5)
1. What is the main benefit of using sealed classes in Kotlin when combined with a when expression?
easy
A. The when expression becomes exhaustive, ensuring all cases are handled.
B. It allows subclassing from any file in the project.
C. It automatically generates getter and setter methods.
D. It makes the class open for inheritance everywhere.

Solution

  1. Step 1: Understand sealed classes restriction

    Sealed classes restrict subclassing to the same file, so all subclasses are known at compile time.
  2. Step 2: Effect on when expression

    This allows the when expression to be exhaustive because the compiler knows all possible subclasses.
  3. Final Answer:

    The when expression becomes exhaustive, ensuring all cases are handled. -> Option A
  4. Quick Check:

    Sealed classes + when = exhaustive check [OK]
Hint: Sealed classes limit subclasses, making when exhaustive [OK]
Common Mistakes:
  • Thinking sealed classes allow subclassing anywhere
  • Confusing sealed classes with data classes
  • Believing sealed classes auto-generate methods
2. Which of the following is the correct syntax to declare a sealed class in Kotlin?
easy
A. sealed class Shape {}
B. open sealed class Shape {}
C. sealed interface Shape {}
D. final sealed class Shape {}

Solution

  1. Step 1: Recall sealed class declaration syntax

    In Kotlin, sealed classes are declared with the keyword sealed followed by class and the class name.
  2. Step 2: Check options for correctness

    sealed class Shape {} uses correct syntax: sealed class Shape {}. open sealed class Shape {} incorrectly adds open, which is not allowed. sealed interface Shape {} uses sealed interface, which is invalid. final sealed class Shape {} uses final sealed class, which is not allowed.
  3. Final Answer:

    sealed class Shape {} -> Option A
  4. Quick Check:

    sealed class declaration = sealed class Shape {} [OK]
Hint: Use only 'sealed class ClassName' to declare sealed classes [OK]
Common Mistakes:
  • Adding 'open' modifier to sealed class
  • Trying to declare sealed interfaces (not allowed)
  • Using 'final' modifier with sealed class
3. Given the sealed class hierarchy below, what will be the output of the describe function when called with Shape.Circle(5.0)?
sealed class Shape {
    data class Circle(val radius: Double) : Shape()
    data class Rectangle(val width: Double, val height: Double) : Shape()
}

fun describe(shape: Shape): String = when(shape) {
    is Shape.Circle -> "Circle with radius ${shape.radius}"
    is Shape.Rectangle -> "Rectangle with width ${shape.width} and height ${shape.height}"
}
medium
A. Rectangle with width 5.0 and height 5.0
B. Compilation error due to missing else branch
C. Shape.Circle
D. Circle with radius 5.0

Solution

  1. Step 1: Identify the input and matching when branch

    The input is Shape.Circle(5.0). The when expression matches is Shape.Circle branch.
  2. Step 2: Evaluate the returned string

    The branch returns the string "Circle with radius 5.0" using the radius property.
  3. Final Answer:

    Circle with radius 5.0 -> Option D
  4. Quick Check:

    Input Circle(5.0) returns "Circle with radius 5.0" [OK]
Hint: Match input type in when to get correct output string [OK]
Common Mistakes:
  • Confusing Circle with Rectangle output
  • Expecting a compilation error due to else missing
  • Returning class name instead of formatted string
4. Consider the following code snippet. What is the error and how can it be fixed?
sealed class Result {
    object Success : Result()
    object Failure : Result()
}

fun handle(result: Result) = when(result) {
    is Result.Success -> "Success"
}
medium
A. Runtime error: Result.Failure is not handled and causes crash.
B. Compilation error: when expression is not exhaustive; add branch for Result.Failure.
C. No error; code runs fine and returns null for Result.Failure.
D. Syntax error: sealed classes cannot have object subclasses.

Solution

  1. Step 1: Identify the sealed class subclasses

    Result has two subclasses: Success and Failure.
  2. Step 2: Check when expression branches

    The when expression only handles Result.Success, missing Result.Failure branch.
  3. Step 3: Understand Kotlin's exhaustiveness check

    Since Result is sealed, when must handle all subclasses or have an else branch. Missing Failure causes compilation error.
  4. Final Answer:

    Compilation error: when expression is not exhaustive; add branch for Result.Failure. -> Option B
  5. Quick Check:

    Sealed class when must cover all subclasses [OK]
Hint: Add all sealed subclasses in when to avoid errors [OK]
Common Mistakes:
  • Assuming runtime error instead of compile error
  • Ignoring missing branches in when
  • Thinking sealed classes can't have object subclasses
5. You have a sealed class Operation with subclasses Add, Subtract, and Multiply. Write a Kotlin function calculate that takes an Operation and two integers, and returns the result of applying the operation. Which of the following implementations correctly uses an exhaustive when expression?
hard
A. fun calculate(op: Operation, a: Int, b: Int): Int = when(op) { is Operation.Add -> a + b else -> 0 }
B. fun calculate(op: Operation, a: Int, b: Int): Int = when(op) { is Operation.Add -> a + b is Operation.Subtract -> a - b }
C. fun calculate(op: Operation, a: Int, b: Int): Int = when(op) { is Operation.Add -> a + b is Operation.Subtract -> a - b is Operation.Multiply -> a * b }
D. fun calculate(op: Operation, a: Int, b: Int): Int = when(op) { is Operation.Multiply -> a * b else -> a + b }

Solution

  1. Step 1: Check all subclasses handled in when

    Operation has Add, Subtract, Multiply. fun calculate(op: Operation, a: Int, b: Int): Int = when(op) { is Operation.Add -> a + b is Operation.Subtract -> a - b is Operation.Multiply -> a * b } handles all three explicitly.
  2. Step 2: Verify exhaustiveness and correctness

    fun calculate(op: Operation, a: Int, b: Int): Int = when(op) { is Operation.Add -> a + b is Operation.Subtract -> a - b is Operation.Multiply -> a * b } covers all subclasses, so when is exhaustive. The other options miss some subclasses or rely on else, which is less clear.
  3. Final Answer:

    fun calculate(op: Operation, a: Int, b: Int): Int = when(op) { is Operation.Add -> a + b is Operation.Subtract -> a - b is Operation.Multiply -> a * b } -> Option C
  4. Quick Check:

    All subclasses covered in when = exhaustive [OK]
Hint: Cover all sealed subclasses explicitly for exhaustive when [OK]
Common Mistakes:
  • Missing some subclasses in when branches
  • Using else instead of explicit branches
  • Returning wrong default values
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard