let / run / apply / also / with
Kotlin’s scope functions (let, run, with, apply, also) execute a block in the context of an object. Picking the right one comes down to two questions: how should the receiver be referenced (this or it) and what should the block return (the object or the block result)?
let, run, with, apply, also, decision
EXAMPLE
// 1) Cheat sheet
// apply — receiver as 'this', returns the receiver → configure-this object
// also — receiver as 'it', returns the receiver → side effects (log, peek)
// run — receiver as 'this', returns block result → transform with member access
// let — receiver as 'it', returns block result → null-safety + transformation
// with — receiver as 'this', returns block result → group calls on one object (not extension)
// 2) apply — common builder-style
data class User(var name: String = "", var email: String = "", var role: String = "")
val user = User().apply {
name = "Mara"
email = "mara@example.com"
role = "admin"
}
// Useful for stamping config on an object you've just constructed.
// 3) also — side effects that should not change the value
val saved = repository.save(user).also {
logger.info("saved user ${it.id}")
}
// 'also' returns the receiver, so the chain continues with the original object.
// Naming it 'it' makes the side-effect intent clear.
// 4) let — null safety + scope
val emailLength = user.email?.let { it.length } ?: 0
fun greet(name: String?) {
name?.let { println("Hello, $it") }
}
// let is ALSO the way to introduce a local val with a transformation:
val doubled = numbers.map { it * 2 }
.let { it.average() } // converts list → double; available in the chain
// 5) run — block result, with member-access via 'this'
val summary = user.run {
"$name <$email> (${role.uppercase()})"
}
// Useful when you want to use the object's members WITHOUT prefixing each access.
// Run can also be called without a receiver — used for grouping setup into a single expression
val port = run {
val raw = System.getenv("PORT")
raw?.toIntOrNull() ?: 8080
}
// 6) with — when the object is a parameter (not an extension)
val config = with(loadConfig()) {
"host=$host port=$port db=$db"
}
// 'with' is a regular function (not extension); reads naturally for one-off grouping.
// 7) Side-by-side comparison
class Order(var id: String = "", var amount: Long = 0L)
val o1 = Order().apply { id = "o-1"; amount = 1000 } // returns Order
val o2 = Order().also { it.id = "o-2"; it.amount = 2000 } // returns Order
val s1 = Order().run { "$id: $amount" } // returns String
val s2 = Order().let { "${it.id}: ${it.amount}" } // returns String
val s3 = with(Order()) { "$id: $amount" } // returns String
// 8) Decision tree
// need to RETURN the same object?
// yes → use receiver as 'this' (mutating many props)? apply
// side effect, peek? also
// need to RETURN the BLOCK result?
// yes → access members easily as 'this'? run
// handle null with ?. + transform? let
// object isn't an extension target (e.g. constructed inline)? with
// 9) Chaining safely
user?.takeIf { it.active }?.let { /* only runs if non-null and active */ }
?.also { audit.recordRead(it.id) }
?.run { "$name ($role)" }
?.also { println(it) }
// 10) Idiomatic Android usage
val intent = Intent(this, DetailActivity::class.java).apply {
putExtra("id", productId)
putExtra("source", "feed")
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
startActivity(intent)
binding.title.text = product.run { "${title} — ${formattedPrice()}" }
// 11) Server-side example
fun ApplicationCall.respondError(status: HttpStatusCode, message: String) =
response.status(status).also {
respond(HttpStatusCode.fromValue(status.value), mapOf("error" to message))
}
// 12) Patterns to AVOID
// • Nesting scope functions 3+ deep — refactor into named functions
// • Using 'apply' just because — if you don't need the receiver back, prefer 'run' or 'let'
// • Switching between 'this' and 'it' to make the line shorter — readability beats brevity
// • Naming a real receiver 'it' inside a long block — name it explicitly: .let { user -> … }
// • Using 'with' on long expressions — assign to a val first or use the extension form (apply/run/also)
// 13) Null handling — let beats safe-call chains
val token = request.headers["Authorization"]
?.removePrefix("Bearer ")
?.takeIf { it.isNotBlank() }
?.let { tokens.verify(it) }
?: return Unauthorized
// 14) Mixed usage in a constructor / DSL
class Query private constructor() {
var select: List<String> = emptyList()
var from: String = ""
var where: String? = null
companion object {
fun build(block: Query.() -> Unit): Query = Query().apply(block)
}
}
val q = Query.build {
select = listOf("id", "name")
from = "users"
where = "active = true"
}
// 15) Performance note
// All five scope functions are 'inline' — at compile time they unfold into regular blocks with no
// allocations. Performance is the same as writing the code by hand. Pick by readability, not perf.
// 16) Common bugs
// • Mixing up apply and also — apply is 'this', also is 'it'; readers can't tell at a glance which property is being set
// • Using let on a non-nullable receiver — the safe-call is the value; let is for the conditional/transform aspect, not the null check
// • Returning the wrong value from run/with — common when adding statements after the result
// • Naming the parameter 'it' inside a long block — rename explicitly when scope grows
// • Apply chain modifying the object then forgetting that side-effecting calls don't update 'it' in 'also' — easy to spot in code review
Why it matters
Choose scope functions by what they return (apply/also return the receiver, run/let/with return the block) and how the receiver is referenced (this for apply/run/with, it for let/also). apply for builder-style config, also for logging side effects, let for null safety, run for terse derivations.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
user.let { println(it.name) }
user.also { println("created $it") }
user.apply { age = 37 } // modify, return user
val msg = user.run { "$name ($age)" } // compute from user
Try it Yourself »
Exercise
Run a block and return its receiver modified.
user.
{ age = 37 }
Five letters.
Discussion
Loading…