Blog Infos
Author
Published
Topics
, , , ,
Published

 

Kotlin 2.2 is in RC2 stage, and soon we’ll get it. I saw many (not all of them I read) articles about the introduction of “when guards”. Most shows the syntax, which is pretty technical. However, with the introduction of this enhancement, it is more interesting to look at the philosophy behind language design.

The first time I heard about guards in Kotlin, I thought — Yes! It should be the same way as in Swift. Right? Well… I was a bit disappointed (because well.. KMP, you know… attracting iOS devs to Kotlin… ) when I read how it was actually implemented. I was questioning, what is the point? We can do the same already, and there is no point in naming it “feature”… or not?

In mobile, two languages are primary today — Swift and Kotlin.
Swift introduced its own guard feature quite early, aiming to handle errors and edge cases in a better way. Here is simple example of narrowing a nullable type and checking if it matches a more specific condition:

func handle(anythingYouCanThinkAbout: BasicTypeOfAnything?) {
 guard let safeData = anythingYouCanThinkAbout, anythingYouCanThinkAbout is SomethingNarrow else {
 // This does not work, run away from here, we have no choice
 // in Swift
 return
 }
 // We can do something good with the data
}

Kotlin guards conditions in when with subject (yeah kind of official naming):

fun handle(data: BasicTypeOfAnything?) {
 when (data) {
 is SomethingNarrow if data is SomethingEvenMoreSpecific -> {
 // We can do something good with the data
 }
 else -> {
 // This does not work, run away from here? 
 // ah no! We are allowed not to do this in Kotlin.
 }
 }
}

Kotlin does not introduce a separate keyword for early returns like Swift’s guard. Instead, it builds on top of existing when expressions by allowing additional conditions with if. This supposed to keep Kotlin’s design compact.

How does the new design compare to old options, and does it truly offer something better?

The same logic could be implemented using a when expression without a subject:

when {
 value is SomethingNarrow && value is SomethingEvenMoreSpecific -> {
 // We can do something good with the data
 }
 else -> {
 // Something else
 }
}

or

value?.let {
 when {
 it is SomethingNarrow && it is SomethingEvenMoreSpecific -> {
 // We can do something good with the data
 }
 else -> {
 // Something else
 }
 }
}

These forms are already readable and flexible. They support complete expression logic, work well with smart casts, and are part of the existing language rules. So what is the gain with when guards?

This is where the philosophical aspect of language comes in. Kotlin facilitates readability and compact syntax. A when(value) block clearly communicates that we are branching based on value. By embedding the conditions inside when we make this dependency explicit.

The syntax rules around guards introduce restrictions that are not intuitive.

(I need this class for demonstration)

sealed class Shape {
   data class Circle(val radius: Double) : Shape()
   data class Ellipse(val radius: Double) : Shape()
   data class Rectangle(val width: Double, val height: Double) : Shape()
}

1. You cannot mix multiple when conditions using commas with an if guard. The following code will not compile:

when (shape) {
 is Shape.Circle, is Shape.Ellipse if shape.radius > 10 -> // Big round shape
 // Error: guard cannot be combined with multiple entries
}

2. To express the same logic, developers must duplicate branches (this comes from 1.):

when (shape) {
 is Shape.Circle if shape.radius > 10 -> // Big round shape
 is Shape.Ellipse if shape.radius > 10 -> // Big not-that-round shape
 else -> // Other shape
}

3. The else if construct is not equivalent to a regular else followed by if . It is a special syntax used only in when guards, and looks like this:

when (shape) {
 is Shape.Rectangle -> // Rectangle?
 else if shape is Shape.Circle && shape.radius > 10 -> // Big round shape
 else -> // Could be a hamster
}

These new constructs differ from existing patterns. Although they improve the expressiveness of when, they also raise the bar for entrance.

To see this complexity more clearly, look at this:

With when guards:

when (shape) {
 is Shape.Circle if shape.radius > 0 -> return
 // compiler error: ‘when’ expression must be exhaustive
}

Compare this to the old way:

when {
 shape is Shape.Circle && shape.radius > 10 -> return
 // no compiler warning about exhaustiveness
}

This demonstrates an important distinction. In the new when guard form, Kotlin expects exhaustiveness to be preserved, even if you are only interested in a subset of a subtype. The compiler does not treat a guarded condition as fully covering that subtype, so it still expects an else or additional branches.

when without a subject lets you write arbitrary expressions without this constraint. The control flow no longer explicitly shows that it’s based on shape, and the smart cast logic is obscure to read.

And again, the question.

What are we trading here — clarity for structure, or structure for clarity?

It triggers a deeper question about the design philosophy of the language. Should Kotlin keep layering enhancements into existing constructs, like when, making them more powerful but more complex? Or should it leave such power to the more general-purpose if/when without subject forms, which are already expressive?

Is this a new dilemma? Not really. The idea of guards in control structures is far from new. The concept of guarded conditions in control flow dates back to one of the most influential computer scientists of the 20th century: Edsger W. Dijkstra.

Dijkstra introduced the concept of guarded commands as part of his proposal for a new language construct designed to simplify nondeterministic programming. That approach was designed to make it easier to express programs where the next step in execution depends on multiple possible conditions, any of which could be valid.

Each guarded command takes the form:

guard -> command

The meaning is simple. If the guard (a boolean expression) is true, then the corresponding command may execute. When multiple guards are true, the choice among them is nondeterministic — the language or runtime may pick any of them. This model was elegant because it allowed for writing correct and concise specifications of complex programs without hardcoding control flow too early. At the time, many programming models tightly coupled decision logic and execution order, making it hard to express alternatives without deeply nested if-chains or flag-based branching. Dijkstra’s approach broke that ‘normal’ by suggesting that you don’t always need to know the exact next step — you just need to describe the valid possibilities and let the runtime (or verification process) choose among them.

Dijkstra’s guards were more than just a syntactic convenience — they were a way of expressing possibilities, and shifting the pressure of choosing between them from the programmer to the program logic itself. I’d say it was declarative before “declarative” was cool.

So far, as you see, Kotlin’s when guards are deterministic and not tied to concurrent or nondeterministic execution, the philosophical influence is clear: they allow developers to attach conditions to branches in a way that makes control flow more expressive, compact, and logically scoped.

But Dijkstra’s idea was not the end of the road for guards. In the following decades, the concept found new life and purpose in functional languages, which shaped it into a more practical, pattern-oriented construct. Many languages have adapted and redefined guards for everyday coding.

Haskell is closest to Dijkstra’s ideas. This guard’s implementation closely mirrors Dijkstra’s idea of specifying valid branches rather than enforcing exact control flow.

In Haskell, guards are used to write conditional logic in a more declarative, readable way within function definitions. Instead of using nested if expressions, Haskell allows a series of conditions to be attached directly to function clauses:

handle value
 | isSomethingNarrow value && isSomethingEvenMoreSpecific value = “We can do something good with the data”
 | otherwise = “Something else”

Each guard is introduced with a |, and evaluated top to bottom. This style is concise and makes the branching logic explicit. otherwise keyword is just an alias for true, and acts like Kotlin’s else.

Another example is from Scala. Scala is often seen as a hybrid language that combines object-oriented and functional paradigms. It provides a practical and concise form of guards inside match expressions. In Scala, guards let developers refine pattern matches with extra conditions without leaving the context of structured branching. This makes guards in Scala highly ergonomic and expressive, especially for those coming from imperative or Java-like languages. As such, Scala plays a bridge role — showing how ideas from functional programming can live comfortably in general-purpose, pragmatic languages.

data match {
 case d: SomethingNarrow if d.isInstanceOf[SomethingEvenMoreSpecific] =>
 // We can do something good with the data
 case _ =>
 // Something else
}

These examples share a common trait with Dijkstra’s idea: the branching is conditional, declarative, and refined in place. But they also deviate from the original nondeterministic model. Instead, Haskell and Scala integrate guards as ergonomic tools for deterministic pattern-based branching.

Kotlin’s when guards fall somewhere in the middle: they borrow syntax and intent from these functional languages, while staying embedded in Kotlin’s imperative model. Their purpose is not to introduce a new paradigm, but to extend an existing when.

Just for a moment — Swift took a different path.

Swift’s guard statement was introduced not as an extension of pattern matching, but as a control-flow primitive for early exits. Unlike Dijkstra’s guarded commands or Haskell-style inline refinements, Swift’s guard is more about protecting the happy path:

func handle(shape: Shape?) {
 guard let shape = shape else {
 // You shaaaaaaall not paaaaaas!11
 return
 }
 // Good to go
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Kotlin Multiplatform Alchemy Making Gold out of Your Swift Interop

Kotlin Multiplatform has been stable since October 2023, and many companies have already started introducing this exciting technology in their projects.
Watch Video

Kotlin Multiplatform Alchemy Making Gold out of Your Swift Interop

Pamela Hill
Developer Advocate
JetBrains

Kotlin Multiplatform Alchemy Making Gold out of Your Swift Interop

Pamela Hill
Developer Advocate
JetBrains

Kotlin Multiplatform Alchemy Making Gold out of Your Swift Interop

Pamela Hill
Developer Advocate
JetBrains

Jobs

This syntax is readable, but if you look at it through the historical and philosophical lens, you find that Swift’s guard is somewhat of an outlier. It doesn’t participate in pattern refinement like Haskell or Scala, nor does it expand the shape of existing syntax like Kotlin’s when. Instead, guard forms a new structural unit with strict semantics: the condition must fail, and scope must exit.

In this sense, guard is imperative, sequential, and flow-oriented. It reflects Swift’s overall design tendency toward explicit control and readability over composability. It’s declarative in feel, but not in origin (Comments section is below. Welcome.)

Swift’s design solves a very real problem: how to make early exits visible and pleasant. But it doesn’t follow the same evolution as Kotlin. Rather than a refinement of match-based logic, Swift’s guard is a rethinking of if and turned inside out.

This contrast is critical for understanding why Kotlin did not adopt this guard statement. Kotlin tends to evolve by extending constructs already familiar to developers, rather than introducing entirely new paradigms. Not all the time for sure! However, there is evidence in features that have been part of Kotlin since version 1.0 like the Elvis operator (?:), safe calls (?.), and the when expression itself. None of these were invented from scratch. Instead, each distilled a widely used idiom (null-checking, defaulting, branching) into a more consistent form that integrated smoothly into the language’s expression-oriented design.

In the case of when guards, Kotlin already provided multiple ways to write complex conditionals and match logic: with if, with when without subject, and with smart casting. What it didn’t offer was a concise and idiomatic way to combine type-based branching with additional conditions directly in a when subject block. I think that when guards were introduced to refine this edge case — not to replace existing patterns, but to make a common structure more readable and discoverable.

Do we need them? Maybe not always.

But do they fit Kotlin’s evolution? I think — absolutely.

It’s quite possible that the real value of when guards will become clear only after the release of Rich Errors in Kotlin 2.4.

This article was previously published on proandroiddev.com.

Menu