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/elsefor boolean dispatch. - Use
ifas expression — the conventional ternary substitute. - Use
whenfor 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/whenexpressions admit substantial conciseness. - Use
tryas 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.