skip to content

let and run both return the lambda result. How do you choose between them, and how does the this-vs-it distinction affect chaining and null safety?

level: middleimportance: should knowfreq 60%

answer

  1. let=it+result, run=this+result
  2. let for transform/pass-on/rename/nest
  3. run for many member calls
  4. both chain with ?. for null safety
  5. run{} (no receiver) scopes a computed val

basics

~20 s

Both give back the block's last line. Pick let when you want the object as a named 'it' (good for passing it around or null-safe checks). Pick run when you want it as 'this' so you can call its members directly.

solid answer

~40 s

let and run share Axis 2 (return the lambda result) and differ on Axis 1: let exposes it (argument), run exposes this (receiver). Choose let when you transform/map a value, pass the object onward (it), rename it for clarity, or nest scopes. Choose run when you make several member calls on the object and want them unqualified, or when run reads as 'execute this block and yield a result'. Both have extension forms (x.let{}, x.run{}) that chain with the safe call ?., so x?.let{} and x?.run{} both execute only when x is non-null and yield a nullable result. run additionally has a non-extension form run { ... } that simply runs a block and returns its value, with no context object — handy for scoping local computations or initializing a val.

code

kotlin · 14 lines
kotlin
val raw: String? = readLine()

// let: transform via named it, null-safe
val id: Int? = raw?.let { it.trim().toIntOrNull() }

// run: several member calls unqualified, null-safe
val shout: String? = raw?.run { trim().uppercase() }

// run with no receiver: scope a multi-step initialization
val port = run {
    val base = 8000
    val offset = 80
    base + offset
}

go deeper

for a junior

Knows both return the block result and that let uses it, run uses this.

for a middle

Chooses correctly per situation and uses ?.let for null safety idiomatically.

for a senior

Explains the no-receiver run { } form and why with can't chain with ?. while run can.

for a principal

Advises on readability conventions (prefer let when passing the object on; reserve run for member-heavy blocks) and avoids over-chaining.

## Shared trait: return the lambda result Both functions return whatever the block's last expression evaluates to: ```kotlin public inline fun <T, R> T.let(block: (T) -> R): R = block(this) public inline fun <T, R> T.run(block: T.() -> R): R = block() ``` `let` takes `(T) -> R` → object is the parameter `it`. `run` takes `T.() -> R` → object is the receiver `this`. Both are `inline`. ## When `let` shines - **Transforming a value:** `val name = user.let { it.firstName + " " + it.lastName }`. - **Passing the object as an argument:** `path.let(::readFile)` or `it.save()` reads naturally because `it` is a real parameter. - **Renaming for clarity / nesting:** `outer.let { o -> inner.let { i -> o.merge(i) } }` — two distinct names avoid confusion that nested `this` would cause. - **Null-safe mapping:** the classic `nullable?.let { /* runs only if non-null */ }`. ## When `run` shines - **Many member calls:** `service.run { connect(); authenticate(); fetch() }` — no `it.` noise. - **As a no-receiver block expression:** `val config = run { val a = compute(); val b = a * 2; Config(a, b) }`. This `run { }` form has no context object; it just creates a scope and returns a value — useful for initializing a `val` with multi-step logic. ## Null safety and chaining Both extension forms participate in the safe-call operator `?.`: ```kotlin val len: Int? = maybeString?.let { it.length } // null if maybeString is null val up: String? = maybeString?.run { uppercase() } // null if maybeString is null ``` The whole expression short-circuits to `null` when the receiver is null, because `?.` skips the call. This makes `?.let` the canonical idiom for *do something only if non-null*. Contrast with `with(x) { }`, a plain function with no receiver-extension form, so it cannot be written as `x?.with{}`. ## Picking between them - Need the object as a **named argument** / passing it on / renaming / nesting → **let**. - Need **clean member access** on the object → **run**. - Need a **block with no context object** to compute a value → **run { }** (non-extension). The return value is identical in spirit; the choice is about whether `it` or `this` makes the block read better.

  • What does run { } with no object before it do?
    It runs the block as a scope with no context object and returns its last expression — useful for multi-step val initialization.
  • Why can you write x?.let{} and x?.run{} but not x?.with{}?
    let and run have extension forms (T.let / T.run) so the safe call applies; with is a top-level function taking the object as an argument, with no extension receiver.

let labels the object and asks 'do something with it'; run mounts the object as 'this' and says 'do your thing here'.

saying these in an interview costs you the question

  • Saying run returns the original object
  • Claiming let cannot be used for null safety
  • Not knowing run has a no-receiver form
  • Thinking with chains with ?. like run
  • Believing let always allocates a lambda (it's inline)

context