Scope Functions (let, run, apply, also, with)
Reviewed & published by Brayan K
Kotlin is a modern, concise language whose five scope functions — let, run, apply, also, and with — let you operate on an object inside a temporary scope for cleaner, more expressive code.
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll know which scope function to reach for, based on how it references the object and what it returns.
What You'll Learn in This Lesson
- let — transform a value, returns the result
- run — operate via this, returns the result
- apply — configure an object, returns the object
- also — side effects, returns the object
- with — operate on an argument via this
- Choosing by receiver (it/this) and return value
1️⃣ let and run
let exposes the object as it and returns the lambda's result — perfect for null-safe transforms. run exposes it as this and also returns the result — handy when you access members directly and want a computed value back.
data class Person(var name: String, var age: Int)
fun main() {
// let: refers to the object as 'it', RETURNS the lambda result.
// Great for null-safety and transforming a value.
val nameLength = "Kotlin".let { it.length }
println(nameLength) // -> 6
val maybe: String? = "hi"
maybe?.let { println("Got: $it") } // runs only if not null -> Got: hi
// run: refers to the object as 'this', RETURNS the lambda result.
val person = Person("Ada", 36)
val summary = person.run {
age += 1 // 'this' is implicit
"$name is now $age"
}
println(summary) // -> Ada is now 37
}The maybe?.let { } pattern runs the block only when the value is present — a clean replacement for a null check.
2️⃣ apply, also, and with
apply (this receiver) returns the object — ideal for configuration. also (it receiver) also returns the object — ideal for side effects like logging. with takes the object as an argument and returns the lambda result.
data class Server(var host: String = "", var port: Int = 0)
fun main() {
// apply: 'this' receiver, RETURNS the object itself. Ideal for setup.
val server = Server().apply {
host = "localhost"
port = 8080
}
println(server) // -> Server(host=localhost, port=8080)
// also: 'it' receiver, RETURNS the object. Good for side effects.
val nums = mutableListOf(1, 2)
.also { println("Before: $it") }
.apply { add(3) }
println(nums) // -> [1, 2, 3]
// with: not an extension; takes the object as an argument,
// 'this' receiver, RETURNS the lambda result.
val description = with(server) {
"Serving $host on port $port"
}
println(description) // -> Serving localhost on port 8080
}Because apply and also return the object, they chain beautifully — set up, peek, and keep going in one fluent expression.
Your turn. Replace the TODO, then run and compare.
// 🎯 YOUR TURN — replace each TODO then run main()
data class Config(var name: String = "", var retries: Int = 0)
fun main() {
// 1) Use apply to configure the object and return it.
val config = Config().TODO { // 👉 replace TODO with 'apply'
name = "prod"
retries = 3
}
println(config) // expect Config(name=prod, retries=3)
// 2) Use let to transform a nullable value safely.
val input: String? = "abc"
val len = input?.let { it.length } ?: 0
println("Length: $len") // expect Length: 3
}Mini-Challenge: Build and Describe a User
Use apply to construct, with to describe, and let for a nullable.
// 🎯 MINI-CHALLENGE: build and describe a user.
// Save as Scope.kt and run main().
//
// data class User(var name: String = "", var email: String = "")
//
// 1. Use apply to create a User with name = "Sam" and
// email = "sam@x.com".
// 2. Use with(user) to build and print a one-line description:
// "Sam <sam@x.com>".
// 3. Use a nullable val nickname: String? = null and print
// nickname?.let { "Nick: " + it } ?: "No nickname".
//
// ✅ Example output:
// Sam <sam@x.com>
// No nickname
data class User(var name: String = "", var email: String = "")
fun main() {
// your code here
}Common Errors (and the fix)
- Returning the wrong thing — apply/also return the object; let/run/with return the lambda result. Pick the right one.
- Mixing up it and this — let/also use it; run/with/apply use this.
- Over-nesting scope functions — Deeply nested let { } blocks get unreadable. Name parameters or extract a function.
- Using with on a nullable — with isn't a safe call. For nullables, prefer ?.let or ?.run.
Pro Tips
- 💡 Config? apply. Set several properties on a fresh object and get it back.
- 💡 Null + transform? let. value?.let { ... } is the cleanest guarded transform.
- 💡 Peek mid-chain? also. Log or validate without breaking the fluent flow.
📋 Quick Reference — Scope Functions
| Function | Receiver | Returns |
|---|---|---|
| let | it | lambda result |
| run | this | lambda result |
| with | this (arg) | lambda result |
| apply | this | the object |
| also | it | the object |
🎉 Lesson Complete!
- ✅ let/run/with return the lambda result
- ✅ apply/also return the object — great for chaining
- ✅ let/also use it; run/with/apply use this
- ✅ ?.let guards nullables cleanly
- ✅ Choose by receiver and return value to express intent
Practice quiz
How many scope functions does Kotlin's standard library provide?
- Three
- Five: let, run, with, apply, also
- Two: let and run
- Seven
Answer: Five: let, run, with, apply, also. The five scope functions are let, run, with, apply, and also.
Which two axes distinguish the scope functions?
- Speed and memory
- How the object is referenced (it/this) and what is returned
- Nullability and mutability
- Whether they are inline or not
Answer: How the object is referenced (it/this) and what is returned. They differ by receiver reference (it vs this) and return value (lambda result vs object).
What does apply return?
- The lambda result
- Nothing (Unit)
- The object itself
- A nullable copy
Answer: The object itself. apply returns the receiver object, making it ideal for configuration.
Which scope function uses it as the receiver and returns the lambda result?
- let
- apply
- run
- with
Answer: let. let exposes the object as it and returns the lambda's result.
What does run expose the object as, and what does it return?
- it; the object
- this; the lambda result
- it; the lambda result
- this; the object
Answer: this; the lambda result. run uses this as the receiver and returns the lambda result.
Which is the key difference between apply and also?
- apply returns the object, also returns the result
- apply uses this, also uses it (both return the object)
- Only also can be chained
- apply is for nullables only
Answer: apply uses this, also uses it (both return the object). Both return the object; apply uses this (config), also uses it (side effects).
What is the cleanest way to run a block only when a nullable value is non-null?
- with(value) { }
- value.run { }
- value?.let { }
- value!!.apply { }
Answer: value?.let { }. value?.let { } combines a safe call with let so the block runs only for a present value.
How is with different from the other four scope functions?
- It is not an extension; it takes the object as an argument
- It returns the object
- It uses it as the receiver
- It cannot return a value
Answer: It is not an extension; it takes the object as an argument. with is a regular function taking the object as an argument; it uses this and returns the lambda result.
Which scope function is best for object configuration that returns the configured object?
- let
- with
- apply
- also
Answer: apply. apply uses this and returns the object, perfect for setting several properties.
Which functions expose the object as it (rather than this)?
- run and with
- let and also
- apply and run
- with and apply
Answer: let and also. let and also use it; run, with, and apply use this.
Continue this course
- Previous: Extension Functions
- Next: Coroutines Intro — Write asynchronous, non-blocking code with suspend functions
- Quick reference: Kotlin cheat sheet
Frequently asked questions
What are scope functions and why are there five of them?
Scope functions — let, run, with, apply, and also — execute a block of code on an object and give you a temporary scope to work in. They exist to make code that operates on a single object more concise and readable. There are five because they differ along two axes: how you reference the object inside the block (as it or as this), and what the call returns (the lambda's result or the object itself). Picking the right one expresses your intent clearly.
When do I use 'it' versus 'this' inside the block?
let and also expose the object as it (a normal parameter you can rename), which reads well when you treat the object as an argument or when nesting scopes. run, with, and apply expose it as this (the implicit receiver), so you can access its members directly without a prefix — ideal for configuration where you set many properties. As a rule: use it (let/also) when you mainly call methods on the value, and this (run/with/apply) when you mostly access its members.
What's the difference between apply and also?
Both return the original object (not the lambda result), which makes them perfect for chaining. The difference is the receiver: apply uses this, so it's the go-to for object configuration — Server().apply { host = ...; port = ... }. also uses it, so it's the go-to for side effects that take the object as input, like logging or validation in the middle of a chain — list.also { println(it) }. If you're setting properties, use apply; if you're doing something with the object, use also.
How do let and run help with null safety?
Combining a safe call with let or run runs the block only when the value is non-null: value?.let { ... } executes the lambda just for a present value, with it (or this for run) smart-cast to the non-null type. This is a clean alternative to an if (value != null) check, especially when you also want to transform the value and capture the result. It keeps null-handling localized and expression-based.