Polyglot
Languages Kotlin conditionals
Kotlin § conditionals

Conditionals

Kotlin’s principal conditional construct is if/else if/else — and unlike Java, if is an expression — produces a value. The when expression admits substantial multi-way dispatch with substantial pattern grammar (treated separately in Pattern matching). The condition must be a Boolean (no truthiness coercion); parentheses around the condition are required. Smart casts in conditionals admit substantial type narrowing — the compiler tracks types through is checks. The combination — expression-form if, the when for dispatch, smart casts, the strict-Boolean discipline, the absence of a ternary (the expression-form admits the use case) — is the substance of Kotlin’s selection surface.

if/else if/else

The principal form:

if (condition) {
    body
} else if (other) {
    body
} else {
    body
}

Examples:

if (x > 0) {
    println("positive")
} else if (x < 0) {
    println("negative")
} else {
    println("zero")
}

The parentheses around the condition are required; the braces around the body are required for multi-statement bodies (admitted to omit for single-statement bodies, but conventional discipline includes them).

if as expression

if produces a value — substitutes for the C-family ternary:

val max = if (a > b) a else b

val status = if (user.isActive) "active"
              else if (user.isPending) "pending"
              else "inactive"

val description = if (n > 0) {
    val doubled = n * 2
    "doubled is $doubled"
} else {
    "non-positive"
}

The block-form returns the last expression’s value; the conventional discipline admits the expression-form for short conditionals.

Strict Boolean

The condition must be Boolean — Kotlin does not admit truthiness:

val n = 5
if (n) { }                                         // ERROR: Int is not Boolean
if (n != 0) { }                                    // OK
if (n > 0) { }                                     // OK

The strictness eliminates the C-family truthiness pitfalls.

Smart casts in conditionals

The compiler tracks types through is checks:

val any: Any = "hello"

if (any is String) {
    println(any.length)                            // smart cast: any is String here
}

if (any !is String) {
    println("not a string")
} else {
    println(any.length)                            // smart cast: any is String here
}

For nullability:

val name: String? = "Alice"

if (name != null) {
    println(name.length)                           // smart cast: name is String
}

For local vals, the smart cast admits substantial conciseness. For mutable properties (var), the smart cast may not apply (see Nullability).

Compound conditions

if (a > 0 && b > 0) {
    /* both positive */
}

if (a > 0 || b > 0) {
    /* at least one positive */
}

if (!isReady) {
    /* not ready */
}

// With null-safety:
if (user != null && user.isActive) {
    /* user is non-null and active */
    user.process()                                 // smart cast admits non-null access
}

if with else if chains

val grade = if (score >= 90) "A"
             else if (score >= 80) "B"
             else if (score >= 70) "C"
             else if (score >= 60) "D"
             else "F"

For substantial dispatch, when is conventionally clearer (treated in Pattern matching).

No ternary operator

Kotlin does not have a ?: ternary (the ?: is the Elvis operator for nullability). The conventional substitute is if as expression:

// In other languages:
// val max = a > b ? a : b

// In Kotlin:
val max = if (a > b) a else b

The mechanism admits substantial conciseness without requiring a separate operator.

Elvis operator ?:

The ?: is not the ternary — admits “use this if non-null, else default”:

val name: String? = null
val display = name ?: "anonymous"                  // "anonymous"

val port = config.port ?: 8080
val timeout = options.timeout ?: defaultTimeout

// With throw:
val name = nullable ?: throw IllegalStateException("name required")

// With return:
fun process(input: String?): String {
    val nonNull = input ?: return "default"
    return nonNull.uppercase()
}

Treated in Nullability.

try as expression

try admits expression form:

val n: Int = try {
    parseInt(input)
} catch (e: NumberFormatException) {
    0
}

val result = try {
    compute()
} catch (e: Exception) {
    log(e)
    null
} finally {
    cleanup()
}

The try block’s last expression is the value (or the catch block’s last expression on caught exception).

when as expression

For multi-way dispatch, when is the conventional Kotlin construct:

val category = when {
    n < 0 -> "negative"
    n == 0 -> "zero"
    n < 100 -> "small"
    else -> "large"
}

val result = when (n) {
    0 -> "zero"
    1, 2, 3 -> "small"
    in 4..10 -> "medium"
    !in 1..100 -> "out of range"
    else -> "large"
}

Treated in Pattern matching.

Common patterns

Early return

fun process(input: String?): String {
    if (input == null) return "null input"
    if (input.isEmpty()) return "empty input"
    if (input.length > 100) return "too long"

    // Main body
    return input.uppercase()
}

The pattern admits substantial linear code paths.

Validation chain

fun validate(form: Form): List<String> {
    val errors = mutableListOf<String>()
    if (form.name.isBlank()) errors.add("name required")
    if (form.email.isBlank()) errors.add("email required")
    if (form.age < 0) errors.add("age must be non-negative")
    return errors
}

Conditional initialisation

val config = if (Config.isDev) {
    DevConfig()
} else {
    ProductionConfig()
}

// Or with Elvis:
val config = loadConfig() ?: defaultConfig

Smart cast chain

fun describe(value: Any?): String {
    if (value == null) return "nothing"
    if (value is String) return "string of length ${value.length}"
    if (value is Int) return "int: $value"
    if (value is List<*>) return "list of ${value.size}"
    return "unknown: $value"
}

For substantial dispatch, when is conventionally clearer:

fun describe(value: Any?): String = when (value) {
    null -> "nothing"
    is String -> "string of length ${value.length}"
    is Int -> "int: $value"
    is List<*> -> "list of ${value.size}"
    else -> "unknown: $value"
}

if-as-expression assignment

val message = if (user.isAuthenticated) {
    "Welcome, ${user.name}"
} else {
    "Please log in"
}

Conditional method call

fun process(input: String?) {
    if (input != null && input.isNotEmpty()) {
        actualProcess(input)                       // smart cast to non-null
    }
}

// Or with let:
fun process(input: String?) {
    input?.takeIf { it.isNotEmpty() }?.let { actualProcess(it) }
}

Default-with-Elvis

val port = config.port ?: 8080
val name = user?.name ?: "anonymous"
val timeout = options.timeout ?: 30.seconds

Validate-and-bail

fun process(input: String?): Int {
    val nonNull = input ?: return -1
    require(nonNull.isNotEmpty()) { "empty input" }
    return nonNull.length
}

The require admits substantial validation; throws IllegalArgumentException.

try as expression for safe operation

val n: Int = try {
    input.toInt()
} catch (e: NumberFormatException) {
    0
}

// Or with toIntOrNull:
val n = input.toIntOrNull() ?: 0

Conditional collection

val items = if (includeArchived) {
    allItems
} else {
    allItems.filter { !it.isArchived }
}

// Or with chained operations:
val items = allItems
    .takeIf { includeArchived } ?: allItems.filter { !it.isArchived }

Smart cast with let

fun greet(name: String?) {
    name?.let {
        println("Hello, $it")                      // smart cast to non-null
    }
}

fun process(input: String?) {
    input?.takeIf { it.isNotBlank() }?.let { actualProcess(it) }
}

?: for null-or-throw

val user = findUser(id) ?: throw NoSuchElementException("user $id not found")
val config = loadConfig() ?: throw IllegalStateException("config not loaded")

Nested if for substantial conditions

fun canVote(person: Person): Boolean {
    if (person.age < 18) return false
    if (!person.isCitizen) return false
    if (person.isFelon && !person.isPardoned) return false
    return true
}

// Or with all/any:
fun canVote(person: Person): Boolean {
    val conditions = listOf(
        person.age >= 18,
        person.isCitizen,
        !person.isFelon || person.isPardoned
    )
    return conditions.all { it }
}

Boolean from chained conditions

val isValidUser = user != null
    && user.isActive
    && !user.isBanned
    && user.permissions.contains("login")

Conditional logging

fun log(message: String) {
    if (Config.debug) println("[DEBUG] $message")
}

// Or as extension:
fun String.logIfDebug() {
    if (Config.debug) println("[DEBUG] $this")
}

if with init-block side effects

val token = if (cache.hasValue("token")) {
    cache.get("token") as String
} else {
    val newToken = generateToken()
    cache.put("token", newToken)
    newToken
}

The expression-form admits substantial logic with side effects.

A note on the absence of ?: ternary

Kotlin reserves ?: for the Elvis operator (null-coalescing). The conventional substitute for ternary:

// Other languages:
// val max = a > b ? a : b

// Kotlin:
val max = if (a > b) a else b

The mechanism admits substantial conciseness; the if-as-expression form is the conventional Kotlin idiom.

A note on the conventional discipline

The contemporary Kotlin conditional advice:

  • Use if/else for boolean dispatch.
  • Use if as expression — the conventional ternary substitute.
  • Use when for multi-way value dispatch.
  • Use ?: (Elvis) for nullable defaults.
  • Use smart casts — let the compiler narrow.
  • Use early returns for precondition validation.
  • Use parentheses around conditions (required).
  • Use braces for multi-statement bodies — conventional discipline.
  • Avoid the temptation to write IIFE for substantial expressions — the if/when expressions admit substantial conciseness.
  • Use try as expression for fallible operations admitting a default.

The combination — if/else with strict-Boolean conditions, the expression-form admitting value-returning conditionals, smart casts in conditionals, the substantial when for multi-way dispatch, the ?: Elvis for nullable defaults — is the substance of Kotlin’s selection surface. The discipline produces clear, type-safe, expressive conditional code with substantial conciseness.