Question
I would like clear examples showing when to use each of Kotlin’s scope functions: run, let, apply, also, and with.
I have already read explanations about the differences between these functions, but I still do not have practical examples that make their use cases easy to understand.
Short Answer
By the end of this page, you will understand what Kotlin scope functions do, how run, let, apply, also, and with differ, and when each one is the best fit. You will also see practical examples, common mistakes, and a small project that helps you use them naturally in real Kotlin code.
Concept
Kotlin’s scope functions let you execute a block of code using an object in a shorter, more readable way.
The five commonly used scope functions are:
letrunapplyalsowith
They all help you work with an object inside a block, but they differ in two important ways:
-
How the object is referenced inside the block
- As
it - As
this
- As
-
What the function returns
- The object itself
- The result of the block
That is the whole idea.
Why this matters
In real code, these functions help with tasks like:
- configuring objects
- avoiding repeated variable names
- handling nullable values safely
- performing extra side effects such as logging
- grouping related operations
The core differences
| Function | Object inside block | Returns | Common use |
|---|---|---|---|
let | it | lambda result | null checks, short transformations |
run | this | lambda result | object operations that compute a result |
apply | this | object itself | object setup/configuration |
also | it | object itself | side effects like logging or debugging |
with | this | lambda result | operate on an existing object without extension chaining |
A simple rule
- Need a result from the block? Think
let,run, orwith. - Need the same object back? Think
applyoralso. - Want to configure fields?
apply - Want to safely use a nullable object?
let - Want to do extra side effects?
also - Want to run multiple member calls and return a computed value?
runorwith
These functions matter because they make Kotlin code more expressive, but only when used clearly. Overusing them can make code harder to read, so choosing the right one is important.
Mental Model
Think of a scope function like inviting an object into a small workspace.
You choose:
- what name the object has in the workspace:
thisorit - what you take out of the workspace: the object itself or a new result
Analogy: working at a desk
Imagine you place an object on your desk.
apply: You adjust the object, set it up, and then take the same object away.also: You keep the object the same, but make notes about it on the side, then take the same object away.let: You examine the object and produce some new result.run: You work with the object closely as if it were your current context, and produce some new result.with: Similar torun, but you already have the object and say, “work with this object for a moment.”
Quick intuition
apply= configure this objectalso= do something extra with this objectlet= use this object and produce a resultrun= operate in this object’s context and produce a resultwith= group operations on this object and produce a result
Syntax and Examples
1. let
let uses it and returns the result of the block.
val name: String? = "Kotlin"
val length = name?.let {
it.length
}
println(length) // 6
This is common for nullable values. If name is not null, the block runs. The result is the length.
2. run
run uses this and returns the result of the block.
data class User(val firstName: String, val lastName: String)
val user = User("Ada", "Lovelace")
val fullName = user.run {
"$firstName $lastName"
}
println(fullName) // Ada Lovelace
Inside run, you can access members directly because the receiver is this.
3. apply
apply uses this and returns the object itself.
data class Person(var name: String = "", var age: Int = 0)
val person = Person().apply {
name = "Sam"
age = 25
}
println(person) // Person(name=Sam, age=25)
This is perfect for object configuration.
4. also
also uses it and returns the object itself.
val numbers = mutableListOf(1, 2, 3)
.also {
println("Before adding: $it")
}
numbers.add(4)
println(numbers) // [1, 2, 3, 4]
also is useful for side effects like logging, validation messages, or debugging.
5. with
with uses this and returns the result of the block.
data class Car(val brand: String, val year: Int)
val car = Car("Toyota", 2020)
val description = with(car) {
"$brand ($year)"
}
println(description) // Toyota (2020)
with is not an extension function. You pass the object as an argument.
One example with all five
data class Config(var host: String = "", var port: Int = 0)
val config = Config().apply {
host = "localhost"
port = 8080
}.also {
println("Created config: $it")
}
val address = config.run {
"$host:$port"
}
val hostLength = config.host.let {
it.length
}
val summary = with(config) {
"Server runs on $host:$port"
}
println(address)
println(hostLength)
println(summary)
This shows the pattern clearly:
applyconfiguresalsologsruncomputes using object contextlettransforms a valuewithgroups work on an object
Step by Step Execution
Consider this example:
data class Box(var value: Int = 0)
val result = Box().apply {
value = 10
}.also {
println("Box after apply: $it")
}.run {
value * 2
}
println(result)
Step by step
Box()creates a new object:
Box(value = 0)
apply { value = 10 }runs.- Inside
apply,thisis theBoxobject. valuebecomes10.applyreturns the same object.
- Inside
Now the object is:
Box(value = 10)
also { println("Box after apply: $it") }runs.- Inside
also, the object is available asit. - It prints the object.
alsoreturns the same object unchanged.
- Inside
Printed output so far:
Box after apply: Box(value=10)
-
run { value * 2 }runs.- Inside
run,thisis theBoxobject. value * 2becomes20.runreturns20.
- Inside
-
resultgets the value20. -
println(result)prints:
20
Final output
Box after apply: Box(value=10)
20
What this example teaches
applychanges the objectalsoperforms a side effectruncomputes and returns a final result
Real World Use Cases
let
Useful when working with nullable values or performing a short transformation.
val email: String? = getEmailFromApi()
val domain = email?.let {
it.substringAfter("@")
}
Common uses:
- null-safe processing
- converting one value into another
- limiting variable scope
run
Useful when you want to do several operations on an object and return one computed result.
data class Order(val price: Double, val tax: Double)
val order = Order(100.0, 10.0)
val total = order.run {
price + tax
}
Common uses:
- deriving values from an object
- formatting output
- combining several member accesses into one result
apply
Useful for building or configuring objects.
val intent = android.content.Intent().apply {
action = "OPEN_PROFILE"
putExtra("userId", 42)
}
Common uses:
- creating DTOs or models
- setting Android views, intents, fragments, dialogs
- test data setup
also
Useful for side effects that should not change the main flow.
val user = loadUser()
.also { println("Loaded user: $it") }
Common uses:
- logging
- debugging
- analytics tracking
- sanity checks during a chain
with
Useful when you already have an object and want to group related operations.
data class Report(val title: String, val pages: Int)
val report = Report("Annual", 12)
val text = with(report) {
"Report: $title, pages: $pages"
}
Common uses:
- formatting text from object properties
- reducing repeated object names
- grouped member access without chaining
Real Codebase Usage
In real projects, developers usually choose scope functions based on intent, not just syntax.
1. Object configuration with apply
A very common pattern is object creation plus setup.
data class ApiConfig(var baseUrl: String = "", var timeout: Int = 0)
val config = ApiConfig().apply {
baseUrl = "https://api.example.com"
timeout = 30
}
This reads like: create this object, configure it, and keep it.
2. Null handling with let
Developers often use let after a safe call.
val token: String? = getToken()
token?.let {
println("Using token: $it")
}
This avoids manual null checks.
3. Side effects with also
also is common in chains where you want extra behavior without changing the result.
val user = fetchUserById(1)
.also { println("Fetched user: $it") }
This is useful for:
- logging
- performance tracing
- debug prints
- analytics events
4. Computing values with run
run is common when code should read like “use this object to calculate something.”
data class Rectangle(val width: Int, val height: Int)
val rectangle = Rectangle(4, 5)
val area = rectangle.run {
width * height
}
5. Grouping operations with with
When an object already exists, with can make a block cleaner.
data class Settings(val theme: String, val language: String)
val settings = Settings("dark", "en")
val label = with(settings) {
"Theme=$theme, Language=$language"
}
Common project patterns
- Builder-like setup:
apply - Null-safe validation or transformation:
let - Logging during pipelines:
also - Produce a summary/result from an object:
run - Avoid repeating an object name many times:
with
Guard clause note
Scope functions should not replace simple code when simple code is clearer. For example, a plain if (value != null) may be better than chaining several nested scope functions. Real codebases value readability more than cleverness.
Common Mistakes
1. Using the wrong function for configuration
If you want to configure an object and keep that object, use apply, not run.
Broken idea:
data class User(var name: String = "")
val user = User().run {
name = "Mia"
}
println(user) // user is Unit, not User
Why it fails:
runreturns the last expression in the block- Here, the assignment returns
Unit
Correct:
val user = User().apply {
name = "Mia"
}
2. Using let when it makes code unclear
Too many nested it references become confusing.
val text: String? = "hello"
text?.let {
println(it)
listOf(it.length).also {
println(it)
}
}
This is hard to read because it means different things in different blocks.
Better:
text?.let { value ->
println(value)
listOf(value.length).also { lengths ->
println(lengths)
}
}
3. Using also to modify object properties as the main purpose
also can modify objects, but it is not the clearest choice for configuration.
Less clear:
val person = Person().also {
it.name = "Ana"
it.age = 30
}
Better:
val person = Person().apply {
name = "Ana"
age = 30
}
4. Confusing run and with
Both use this and return the block result.
- Use
runwhen calling on an object as an extension - Use
withwhen passing the object into the function
Both can work, but consistency matters.
5. Overusing scope functions
This can make code harder to follow.
Overcomplicated:
val result = user?.let {
it.profile?.run {
name.also {
println(it)
}
}
}
Sometimes this is clearer:
if (user != null && user.profile != null) {
println(user.profile.name)
}
6. Forgetting what each function returns
This is the most common mistake.
Remember:
applyandalsoreturn the objectlet,run, andwithreturn the lambda result
Comparisons
| Function | Receiver name | Returns | Best for | Example intent |
|---|---|---|---|---|
let | it | lambda result | null checks, transformations | use this value and produce something |
run | this | lambda result | compute using object context | calculate something from this object |
apply | this | object itself | configuration | set up this object |
also | it | object itself | side effects | log or inspect this object |
with | this | lambda result | grouped operations on existing object | work with this object and return a result |
apply vs also
| Feature | apply | also |
|---|---|---|
| Inside block | this | it |
| Returns | object | object |
| Best use | configuration | side effects |
val person = Person().apply {
name = "Leo"
}
val samePerson = person.also {
println(it)
}
let vs run
| Feature | let | run |
|---|---|---|
| Inside block | it | this |
| Returns | lambda result | lambda result |
| Best use | transform a value, especially nullable | compute using object members directly |
val length = "Kotlin".let { it.length }
val length2 = "Kotlin".run { length }
run vs with
| Feature | run | with |
|---|---|---|
| Type | extension function | regular function |
| Called as | obj.run {} | with(obj) {} |
| Returns | lambda result | lambda result |
val a = car.run { "$brand $year" }
val b = with(car) { "$brand $year" }
Both are similar. The choice is often style and readability.
Cheat Sheet
Quick rules
let= useit, return resultrun= usethis, return resultapply= usethis, return objectalso= useit, return objectwith= usethis, return result
Best use cases
let: null-safe calls and transformationsrun: compute a result from an objectapply: configure an objectalso: logging or side effectswith: group operations on an existing object
Return behavior
val x = "hello".let { it.length } // Int
val y = "hello".run { length } // Int
val z = Person().apply { name = "A" } // Person
val w = Person().also { println(it) } // Person
val q = with("hello") { length } // Int
Receiver style
// it
value.let { println(it) }
value.also { println(it) }
// this
value.run { println(this) }
value.apply { println(this) }
with(value) { println(this) }
Memory trick
- apply: apply changes
- also: also do this
- let: let me use this value
- run: run this code in object context
- with: with this object, do the following
Edge cases
runandwithoften look interchangeableapplyandalsoboth return the original objectletis commonly paired with?.- Do not choose a scope function just because it is shorter; choose the one that makes intent obvious
FAQ
When should I use let in Kotlin?
Use let when you want to work with a value, especially a nullable one, and return a new result from it.
What is the difference between apply and also in Kotlin?
Both return the original object. apply is mainly for configuring the object, while also is mainly for side effects like logging.
What is the difference between run and with in Kotlin?
Both use this and return the lambda result. run is called on an object as an extension function, while with takes the object as an argument.
Why is let often used with nullable values?
Because value?.let { ... } only runs the block if value is not null.
Which scope function should I use to initialize an object?
Usually apply, because it lets you set properties and returns the configured object.
Which scope function is best for logging in a chain?
Usually also, because it returns the original object unchanged after the side effect.
Can I use scope functions everywhere?
You can, but you should not overuse them. If plain code is clearer, prefer plain code.
Are Kotlin scope functions faster than normal code?
They are mainly about readability and expressiveness. Performance differences are usually not the reason to choose them.
Mini Project
Description
Create a small Kotlin program that builds a user profile, logs progress, safely handles an optional nickname, and generates a final summary string. This project demonstrates the natural role of apply, also, let, run, and with in one practical flow.
Goal
Build and configure a profile object, perform side effects, handle a nullable value, and produce summary text using the correct Kotlin scope functions.
Requirements
- Create a
Profiledata class with mutable properties for name, age, city, and nickname. - Use
applyto create and configure aProfileobject. - Use
alsoto print the object after configuration. - Use
letto safely format the nickname only if it is not null. - Use
runorwithto generate a final summary string from the profile.
Keep learning
Related questions
Accessing Kotlin Extension Functions from Java
Learn how Kotlin extension functions are compiled and how to call them correctly from Java with clear examples and common pitfalls.
Add a Maven Repository URL in Gradle Kotlin DSL (build.gradle.kts)
Learn how to add Maven Central and a custom Maven repository URL in Gradle Kotlin DSL using build.gradle.kts syntax.
Allow HTTP and HTTPS in Android 9 Pie with Network Security Configuration
Learn how Android 9 Pie handles cleartext HTTP traffic and how to allow HTTP and HTTPS safely using network security config.