How do I use takeIf and takeUnless for conditional evaluations in Kotlin?

In Kotlin, takeIf and takeUnless are scope-style functions used to keep or discard a value based on a condition.

takeIf

takeIf returns the object itself if the predicate is true; otherwise it returns null.

val result = value.takeIf { condition }

Equivalent to:

val result = if (condition) value else null

Example

val number = 10

val evenNumber = number.takeIf { it % 2 == 0 }

println(evenNumber) // 10

If the condition fails:

val number = 7

val evenNumber = number.takeIf { it % 2 == 0 }

println(evenNumber) // null

takeUnless

takeUnless is the opposite of takeIf.

It returns the object itself if the predicate is false; otherwise it returns null.

val result = value.takeUnless { condition }

Equivalent to:

val result = if (!condition) value else null

Example

val number = 7

val notEvenNumber = number.takeUnless { it % 2 == 0 }

println(notEvenNumber) // 7

If the condition is true:

val number = 10

val notEvenNumber = number.takeUnless { it % 2 == 0 }

println(notEvenNumber) // null

Common use with safe calls

Because both functions can return null, they are often used with ?.let.

val input = "kotlin"

input
    .takeIf { it.length > 3 }
    ?.let {
        println("Valid input: $it")
    }

This prints:

Valid input: kotlin

If the condition fails, let is skipped:

val input = "hi"

input
    .takeIf { it.length > 3 }
    ?.let {
        println("Valid input: $it")
    }

Nothing is printed.

Practical examples

Validate a string

fun normalizeUsername(username: String): String? {
    return username
        .trim()
        .takeIf { it.length >= 3 }
        ?.lowercase()
}

Usage:

println(normalizeUsername("  Alice  ")) // alice
println(normalizeUsername("  a  "))     // null

Reject blank input

val name = userInput.takeUnless { it.isBlank() }

This keeps userInput only if it is not blank.

Equivalent to:

val name = if (!userInput.isBlank()) userInput else null

Parse only valid values

val age = input
    .toIntOrNull()
    ?.takeIf { it >= 0 }

This gives you a non-negative integer or null.

println("42".toIntOrNull()?.takeIf { it >= 0 })  // 42
println("-5".toIntOrNull()?.takeIf { it >= 0 })  // null
println("abc".toIntOrNull()?.takeIf { it >= 0 }) // null

Important note

The predicate runs on the object itself, available as it.

val text = "Kotlin"

val result = text.takeIf { it.startsWith("K") }

Here, it is "Kotlin".

When to use which

Use takeIf when you want to keep a value if a condition is true:

val activeUser = user.takeIf { it.isActive }

Use takeUnless when you want to keep a value unless a condition is true:

val visibleUser = user.takeUnless { it.isDeleted }

In short:

value.takeIf { predicate }     // value if predicate is true, else null
value.takeUnless { predicate } // value if predicate is false, else null

How do I chain multiple scope functions together in Kotlin?

In Kotlin, scope functions can be chained because each one returns either:

  • the receiver object: also, apply
  • the lambda result: let, run, with

The key is understanding what each function returns.

Common chaining pattern

val result = User("Alice")
    .also {
        println("Created user: $it")
    }
    .apply {
        name = name.uppercase()
    }
    .let {
        "User name is ${it.name}"
    }

println(result)

Here:

  1. also receives the object as it and returns the same User
  2. apply receives the object as this and returns the same User
  3. let receives the object as it and returns the lambda result, a String

Example with a data class

data class User(var name: String, var age: Int = 0)

val description = User("Alice")
    .apply {
        age = 30
    }
    .also {
        println("Configured user: $it")
    }
    .run {
        "$name is $age years old"
    }

println(description)

Output:

Configured user: User(name=Alice, age=30)
Alice is 30 years old

Choosing the right scope function in a chain

Function Object reference Returns Common use
let it lambda result transform value, null checks
run this lambda result compute result from object
with this lambda result operate on existing object
apply this receiver object configure object
also it receiver object side effects like logging

Nullable chaining

Scope functions are especially useful with nullable values:

val length = maybeName
    ?.trim()
    ?.takeIf { it.isNotEmpty() }
    ?.also {
        println("Valid name: $it")
    }
    ?.let {
        it.length
    }

Here, the chain stops if any step returns null.

Practical example

val user = User(" alice ")
    .apply {
        name = name.trim()
    }
    .also {
        println("After trim: $it")
    }
    .apply {
        name = name.replaceFirstChar { it.uppercase() }
    }
    .also {
        println("Final user: $it")
    }

Rule of thumb

Use:

apply { }

when you want to configure an object and keep chaining the same object.

Use:

also { }

when you want to perform a side effect and keep chaining the same object.

Use:

let { }

or:

run { }

when you want to transform the object into another result.

So a typical chain often looks like:

val result = createObject()
    .apply {
        // configure object
    }
    .also {
        // log or validate
    }
    .let {
        // transform into final result
    }

How do I choose the right scope function for different use cases in Kotlin?

Kotlin scope functions all do the same broad thing: they execute a block of code in the context of an object. The main differences are:

  1. How you refer to the object: this or it
  2. What the function returns: the object itself or the block result

Quick decision table

Function Object reference Returns Best for
let it Lambda result Transforming a value, null-safe execution
run this Lambda result Computing a result from an object
with this Lambda result Grouping operations on an existing non-null object
apply this The original object Configuring or initializing an object
also it The original object Side effects like logging, validation, debugging

Use let when you want to transform a value

let is useful when you want to take an object and produce another value.

val name = "kotlin"

val length = name.let {
    it.uppercase().length
}

println(length) // 6

It is also very common with nullable values:

val email: String? = "[email protected]"

email?.let {
    println("Sending email to $it")
}

Use let when you are thinking:

“If this value exists, do something with it or turn it into something else.”


Use apply when you want to configure an object

apply returns the original object, so it is ideal for initialization.

data class User(
    var name: String = "",
    var age: Int = 0,
    var active: Boolean = false
)

val user = User().apply {
    name = "Alice"
    age = 30
    active = true
}

Inside apply, the object is available as this, so you can access its properties directly.

Use apply when you are thinking:

“Create this object and set it up.”


Use also when you want side effects without changing the chain result

also returns the original object, like apply, but the object is referenced as it.

This makes it good for logging, debugging, validation, or extra actions.

val numbers = mutableListOf(1, 2, 3)
    .also {
        println("Original list: $it")
    }
    .apply {
        add(4)
    }
    .also {
        println("Updated list: $it")
    }

println(numbers) // [1, 2, 3, 4]

Use also when you are thinking:

“Do this extra thing, but keep passing the same object along.”


Use run when you want to compute a result using an object

run uses this and returns the lambda result.

val message = StringBuilder().run {
    append("Hello, ")
    append("Kotlin")
    toString()
}

println(message) // Hello, Kotlin

This is useful when you want to perform multiple operations and return a final computed value.

Use run when you are thinking:

“Use this object to calculate a result.”


Use with when you already have an object and want to group operations

with is not called as an extension in the same style as the others. You pass the object as an argument.

val builder = StringBuilder()

val result = with(builder) {
    append("Hello, ")
    append("Kotlin")
    toString()
}

println(result) // Hello, Kotlin

Use with when you are thinking:

“With this existing object, do several things.”

with is usually best for non-null objects. For nullable values, prefer ?.let or ?.run.


The simplest way to choose

Ask two questions:

1. Do you want to return the original object?

If yes:

  • Use apply for configuration
  • Use also for side effects
val user = User().apply {
    name = "Alice"
}
val user = getUser().also {
    println("Loaded user: $it")
}

2. Do you want to return a new result?

If yes:

  • Use let when you prefer it
  • Use run when you prefer this
  • Use with when the object already exists and is non-null
val length = name.let {
    it.length
}
val text = builder.run {
    append("Done")
    toString()
}

Rule of thumb

let   = transform or null-check
run   = compute a result using this object
with  = operate on an existing object
apply = configure an object
also  = perform side effects

Common examples

Null-safe call

val username: String? = "sam"

username?.let {
    println("Username length: ${it.length}")
}

Best choice: let


Object setup

val request = Request().apply {
    method = "GET"
    url = "/users"
}

Best choice: apply


Logging in a chain

val result = loadUsers()
    .also { println("Loaded ${it.size} users") }
    .filter { it.active }

Best choice: also


Build a value from several operations

val summary = users.run {
    val activeCount = count { it.active }
    "Active users: $activeCount"
}

Best choice: run


Group repeated calls on one object

val result = with(StringBuilder()) {
    append("A")
    append("B")
    append("C")
    toString()
}

Best choice: with


Avoid overusing scope functions

Scope functions are useful, but chaining too many can make code harder to read:

val result = user
    .also { println(it) }
    .let { transform(it) }
    .also { save(it) }
    .run { toDto() }

Sometimes plain code is clearer:

println(user)

val transformed = transform(user)
save(transformed)

val result = transformed.toDto()

A good guideline is: use scope functions when they make intent clearer, not just to make code shorter.

How do I use run, apply, also, and with for cleaner Kotlin code?

In Kotlin, run, apply, also, and with are scope functions. They help make code cleaner by giving you a temporary scope around an object.

The main differences are:

Function Object reference Returns Best used for
apply this The original object Configuring an object
also it The original object Side effects, logging, validation
run this Lambda result Computing a result from an object
with this Lambda result Grouping operations on an object

apply: configure an object

Use apply when you want to initialize or configure an object and then keep using that same object.

val user = User().apply {
    name = "Alice"
    age = 30
    active = true
}

This is cleaner than:

val user = User()
user.name = "Alice"
user.age = 30
user.active = true

apply returns the original object:

val button = Button().apply {
    text = "Save"
    isEnabled = true
}

Use apply for:

  • object setup
  • builder-style initialization
  • setting multiple properties

also: perform side effects

Use also when you want to do something extra with an object without changing the main flow.

val user = repository.findUser(id)
    .also {
        logger.info("Loaded user: ${it.name}")
    }

also uses it and returns the original object.

Another example:

val numbers = mutableListOf<Int>()
    .also {
        println("Created list")
    }

Good use cases:

val file = File("data.txt")
    .also {
        require(it.exists()) { "File does not exist" }
    }

Use also for:

  • logging
  • debugging
  • validation
  • side effects that should not change the returned value

run: compute a result from an object

Use run when you want to execute code using an object and return a computed result.

val description = user.run {
    "$name is $age years old"
}

Here, run returns the last expression:

val length = "Kotlin".run {
    lowercase().length
}

run is useful when you want to avoid repeating the object name:

val isAdult = user.run {
    age >= 18
}

Use run for:

  • computing a value
  • using several properties/methods of an object
  • keeping temporary logic contained

with: group operations on an object

with is similar to run, but it is not called as an extension function.

val result = with(user) {
    "$name is $age years old"
}

This:

with(user) {
    println(name)
    println(age)
}

is cleaner than:

println(user.name)
println(user.age)

Use with when you already have an object and want to perform several operations with it.

val summary = with(order) {
    "Order $id has $itemCount items and costs $total"
}

Quick comparison

apply

val person = Person().apply {
    name = "Alice"
    age = 30
}

Meaning:

Configure this object and return the object.


also

val person = Person("Alice")
    .also {
        println("Created person: $it")
    }

Meaning:

Do something with this object and return the object.


run

val label = person.run {
    "$name ($age)"
}

Meaning:

Use this object to compute and return a result.


with

val label = with(person) {
    "$name ($age)"
}

Meaning:

With this object, compute and return a result.


Practical example

Without scope functions:

val request = Request()
request.url = "https://example.com"
request.method = "GET"
request.headers["Accept"] = "application/json"

logger.info("Created request: $request")

val summary = "Request to ${request.url} using ${request.method}"

With scope functions:

val request = Request().apply {
    url = "https://example.com"
    method = "GET"
    headers["Accept"] = "application/json"
}.also {
    logger.info("Created request: $it")
}

val summary = request.run {
    "Request to $url using $method"
}

Rule of thumb

Use this simple guide:

Need to configure an object?        apply
Need side effects like logging?     also
Need to compute a result?           run
Need to operate on an existing obj? with

Or more specifically:

  • Use apply when the lambda modifies or configures the receiver.
  • Use also when the lambda performs side effects and should not affect the returned value.
  • Use run when the lambda produces a new value.
  • Use with when you want to group operations on an existing object.

One warning: avoid overusing them

Scope functions can make code cleaner, but too many nested scope functions can make code harder to read.

Avoid this:

user.apply {
    address?.run {
        city.also {
            logger.info(it)
        }
    }
}

Prefer clearer code when nesting gets confusing:

val address = user.address ?: return
logger.info(address.city)

In short: scope functions are best when they reduce repetition and make intent obvious.

How do I use let to execute code only when a value is not null in Kotlin?

Use ?.let { ... }.

The safe-call operator ?. makes sure let is called only when the value is not null. Inside the let block, the value is available as a non-null value, usually named it.

val name: String? = "Kotlin"

name?.let {
    println("Name is $it")
    println("Length is ${it.length}")
}

If name is "Kotlin", the block runs.

If name is null, the block is skipped.

You can also give the value a clearer name:

val name: String? = "Kotlin"

name?.let { nonNullName ->
    println("Name is $nonNullName")
    println("Length is ${nonNullName.length}")
}

To handle the null case too, combine it with the Elvis operator ?::

val name: String? = null

name?.let {
    println("Name is $it")
} ?: println("Name was null")

So the general pattern is:

nullableValue?.let { value ->
    // Runs only when nullableValue is not null
}