Kotlin Scope Functions: When to Use Which?

Learn Kotlin scope functions with practical examples and clear explanations of let, run, with, apply, and also. This guide explains how each function works, how objects are accessed, what each function returns, and most importantly, when to use which one. Understand the differences through real-life examples and Kotlin code, making it easier to write cleaner, more readable, and maintainable Kotlin programs.

App Development Kotlin 📅 Aug 20, 2026 👁️ 38 Views
Written by Rohan Kumar
Kotlin Scope Functions: When to Use Which?
Learn Kotlin scope functions with practical examples and clear explanations of let, run, with, apply, and also. This guide explains how each function works, how objects are accessed, what each function returns, and most importantly, when to use which one. Understand the differences through real-life examples and Kotlin code, making it easier to write cleaner, more readable, and maintainable Kotlin programs.

When you start learning Kotlin, you will often come across five functions:

  • let

  • run

  • with

  • apply

  • also

These are called scope functions because they execute a block of code within the scope of an object.

At first, they can be confusing because all five functions look similar. The real difference is how you access the object inside the block and what the function returns.

For example:

user.let {
    println(it.name)
}

and:

user.apply {
    name = "Rohan"
}

Both execute code related to user, but they are normally used for different purposes.

In this article, we'll understand all five scope functions using simple real-life examples and Kotlin code. More importantly, we'll learn when to use which scope function.


What Are Scope Functions?

Suppose you have a User object:

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

val user = User(
    name = "Rohan",
    age = 25,
    email = "rohan@example.com"
)

Without a scope function, you might write:

println(user.name)
println(user.email)

With let, you can write:

user.let {
    println(it.name)
    println(it.email)
}

Here, user becomes available inside the block.

The important thing is that Kotlin provides five different scope functions, and they differ in two main ways:

  1. How you access the object

  2. What the function returns

Here's the basic comparison:

Function Object inside block Returns Common purpose
let it Last expression Transform or work with a value
run this Last expression Perform operations and calculate a result
with this Last expression Perform multiple operations on an existing object
apply this The object itself Configure an object
also it The object itself Perform an additional operation

Understanding this table makes the rest much easier.


1. let: Work With a Value

The first function is let.

Inside a let block, the object is available as it.

For example:

val name = "Rohan"

name.let {
    println(it)
}

Here:

it

refers to "Rohan".

You can also give the object a custom name:

name.let { userName ->
    println(userName)
}

This is useful when the value needs to be passed into another operation or transformed.

Real-Life Example

Imagine a restaurant app receives an optional coupon code from the user.

val couponCode: String? = "SAVE20"

You only want to process the coupon if it exists.

You could write:

couponCode?.let {
    println("Applying coupon: $it")
}

If couponCode is null, the block isn't executed.

If it contains "SAVE20", the output is:

Applying coupon: SAVE20

This is one of the most common uses of let:

Use let when you want to work with a value, especially a nullable value, or transform a value into something else.

For example:

val length = couponCode?.let {
    it.length
}

Now length contains the length of the coupon code, or null if the coupon doesn't exist.


2. run: Perform Operations and Return a Result

run is similar to let, but inside the block you normally access the object using this.

For example:

val user = User(
    name = "Rohan",
    age = 25,
    email = "rohan@example.com"
)

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

println(description)

Inside the block, we can directly write:

name
age
email

instead of:

it.name
it.age
it.email

The result of the last expression is returned.

So:

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

produces:

Rohan is 25 years old

Real-Life Example

Imagine an online shopping application calculating the total price of a cart.

data class Cart(
    val itemPrice: Double,
    val quantity: Int,
    val discount: Double
)

val cart = Cart(
    itemPrice = 500.0,
    quantity = 2,
    discount = 100.0
)

We can calculate the final price using run:

val finalPrice = cart.run {
    (itemPrice * quantity) - discount
}

println(finalPrice)

The result is:

900.0

Here, run is useful because we're using the object's properties to calculate a result.

A simple rule is:

Use run when you want to perform several operations using an object and return the result of those operations.


3. with: Perform Multiple Operations on an Object

with looks slightly different because it isn't called using the dot operator.

Instead of:

user.run {
}

we write:

with(user) {
}

Inside the block, the object is available as this.

For example:

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

This is useful when you're going to perform several operations on the same existing object.

Real-Life Example

Imagine you're preparing an invoice.

data class Invoice(
    val customerName: String,
    val amount: Double,
    val invoiceNumber: String
)

val invoice = Invoice(
    customerName = "Rohan",
    amount = 2500.0,
    invoiceNumber = "INV101"
)

You can use:

with(invoice) {
    println("Invoice: $invoiceNumber")
    println("Customer: $customerName")
    println("Amount: ₹$amount")
}

Output:

Invoice: INV101
Customer: Rohan
Amount: ₹2500.0

The important point is that with is generally used when you already have an object and want to perform several operations on it.

Use with when you want to work with an existing object for a group of related operations.


4. apply: Configure an Object

apply is one of the most useful scope functions when creating and configuring objects.

Inside the block, the object is available as this.

The biggest difference is that apply returns the original object.

For example:

val user = User(
    name = "",
    age = 0,
    email = ""
).apply {
    name = "Rohan"
    age = 25
    email = "rohan@example.com"
}

After the block finishes, user is still a User object.

Real-Life Example

Suppose you're creating a product for an e-commerce application.

data class Product(
    var name: String = "",
    var price: Double = 0.0,
    var category: String = ""
)

You can create and configure it like this:

val product = Product().apply {
    name = "Laptop"
    price = 65000.0
    category = "Electronics"
}

Now:

println(product.name)
println(product.price)

produces:

Laptop
65000.0

This is exactly where apply makes sense.

You're not primarily calculating a new value. You're setting up an object.

Use apply when you want to create or configure an object and continue using that same object afterward.


5. also: Do Something Extra

also is similar to apply because it returns the original object.

The difference is that the object is available as it.

For example:

val user = User(
    name = "Rohan",
    age = 25,
    email = "rohan@example.com"
).also {
    println("User created: ${it.name}")
}

The User object is still returned.

Real-Life Example

Imagine an application saves an order.

You want to save the order, but you also want to log some information.

val order = Order(
    id = 101,
    amount = 2500.0
).also {
    println("Order created with ID: ${it.id}")
}

The logging is an additional operation. It isn't the main purpose of creating the object.

This makes also useful for things such as:

  • Logging

  • Debugging

  • Printing information

  • Performing additional checks

  • Adding side operations

For example:

val numbers = mutableListOf(1, 2, 3)
    .also {
        println("Before adding: $it")
    }
    .also {
        it.add(4)
    }

println(numbers)

The original list continues through the chain.

Use also when you want to perform an additional operation while keeping the original object unchanged as the result of the scope function.


let vs run

These two can look very similar.

Consider:

user.let {
    println(it.name)
}

and:

user.run {
    println(name)
}

The main difference is how you reference the object.

With let:

it

With run:

this

let is often convenient when the object should be treated as a value.

run is convenient when you're performing several operations on the object and want to return a result.


apply vs also

This is another common source of confusion.

Both return the original object.

The difference is mainly how you interact with that object and what you're trying to express.

apply

Use apply for configuration:

val product = Product().apply {
    name = "Laptop"
    price = 65000.0
}

also

Use also for an additional operation:

val product = Product().also {
    println("Created product")
}

A simple way to remember it:

apply → Configure the object
also  → Do something extra with the object

run vs with

Both use this inside the block and return the last expression.

The main difference is how they are called.

With run:

user.run {
    println(name)
}

With with:

with(user) {
    println(name)
}

with is commonly used when you already have an object and want to perform several operations on it.

run is also useful when the block itself is being used to calculate a result.

For example:

val message = user.run {
    "User: $name, Age: $age"
}

A Simple Decision Guide

When you're unsure which scope function to use, ask what you are trying to do.

Do you want to work with a value?

Use:

let

Example:

email?.let {
    sendEmail(it)
}

Do you want to calculate or return a result using an object?

Use:

run

Example:

val total = cart.run {
    price * quantity
}

Do you want to perform several operations on an existing object?

Use:

with

Example:

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

Do you want to configure an object?

Use:

apply

Example:

val user = User().apply {
    name = "Rohan"
    age = 25
}

Do you want to perform an additional operation?

Use:

also

Example:

val user = createUser().also {
    println("User created: ${it.name}")
}

The Easiest Way to Remember All Five

You don't need to memorize complicated definitions.

Remember these two questions:

1. How do I access the object?

let   → it
also  → it

run   → this
with  → this
apply → this

2. What does the function return?

let   → Result of the last expression
run   → Result of the last expression
with  → Result of the last expression

apply → Original object
also  → Original object

So the complete picture is:

Function Access object as Returns Think of it as
let it Result Work with a value
run this Result Calculate something
with this Result Work with an existing object
apply this Object Configure an object
also it Object Do something extra

Conclusion

Kotlin scope functions are not five completely different concepts. They are variations of the same basic idea: temporarily work with an object inside a block of code.

The important part is knowing what you want to accomplish.

Use let when you want to work with a value, especially a nullable value.

Use run when you want to perform operations on an object and return a result.

Use with when you want to perform several operations on an existing object.

Use apply when you are configuring or initializing an object and want the object itself back.

Use also when you want to perform an additional operation such as logging or debugging while keeping the original object as the result.

Once you understand these differences, you don't need to choose a scope function because it looks shorter. Choose it based on what your code is actually doing. That will make your Kotlin code easier to understand and maintain.

🔖 Bookmark saved successfully!