A developer writes `value?.let { return }` expecting to return early from a helper. What actually happens, and what is the subtle bug when this idiom is misused?
answer
- Scope functions are inline → bare return is non-local
- ?.let runs only when non-null
- Null guard = `?: return`, not `?.let { return }`
- return@let ends only the block
- Last expression is the block's value
basics
~20 slet, run, apply, etc. are inline, so a bare return inside their lambda returns from the whole enclosing function — not just from the let block. People expect it to stop only the let, which is the bug.
solid answer
~40 sThe scope functions `let`, `run`, `also`, `apply`, `with` are all `inline`, so a bare `return` inside their lambda is a **non-local return** that exits the enclosing function. `value?.let { return }` therefore returns from the surrounding function whenever `value` is non-null (and does nothing when it's null, because the `?.` short-circuits the whole `let`). The subtle bug is assuming `return` only abandons the `let` block while the function keeps running — it does not. If you want to produce a value from the block, use `return@let`. The null-handling asymmetry is also a trap: `value?.let { return }` returns early *only* when `value` is present, the opposite of an early-exit-on-null guard. For a true null guard use `?: return` instead.
code
kotlin · 10 linesfun greet(name: String?): String {
// BUG-prone: returns early only when name is NON-null
name?.let { return "Hi $it" }
return "stranger"
}
fun greet2(name: String?): String {
val n = name ?: return "stranger" // clear null guard
return "Hi $n"
}go deeper
Recognizes return inside let leaves the function, not just the block.
Explains the inline cause and the ?.let { return } null asymmetry, prefers ?: return.
Spots readability hazards of non-local returns in nested scope functions and recommends clearer idioms.
Sets team conventions for scope-function usage and null guards to prevent subtle control-flow bugs at scale.
## Scope functions are inline `let`, `run`, `with`, `apply`, `also` are all declared `inline`. That means a **bare `return`** inside their trailing lambda is a *non-local return*: it exits the **enclosing named function**, not merely the scope-function block. ```kotlin fun describe(name: String?): String { name?.let { return "Hello, $it" // returns from describe(), not just let } return "Anonymous" } ``` This works as intended here. The trap appears when developers think `return` only ends the `let`. ## The null-short-circuit asymmetry With `?.let`, the lambda runs **only when the receiver is non-null**. So: ```kotlin user?.let { return } // returns ONLY if user != null ``` If you actually wanted to bail out **when the value is null**, this is backwards. The idiomatic null guard is the Elvis operator: ```kotlin val user = repo.find(id) ?: return // early-return when null user.activate() ``` ## Returning from just the block To end only the scope-function lambda and let the function continue, qualify the return: ```kotlin list.map { it.value?.run { if (isBlank()) return@run "empty" uppercase() } ?: "none" } ``` Note that `let`/`run` already *return their last expression* as the block's value, so you rarely need `return@let` at all — just make the last line the result. ## Why people get burned - They expect `return` to be local to the block (it is non-local). - They use `?.let { return ... }` for null guards and get inverted logic. - In deeply nested scope functions, a bare `return` can jump much further out than intended, hurting readability. ## Key terms - **Scope function**: `let`/`run`/`with`/`apply`/`also` — run a block in the context of a receiver. - **Non-local return**: bare `return` exiting the enclosing function (enabled by inlining). - **Elvis operator `?:`**: provides a fallback / early return when the left side is null. - **`return@let`**: qualified return ending only the scope block.
- How would you rewrite `value?.let { return }` so it only exits the block, keeping the function going?Use a qualified return: `value?.let { return@let }` — though usually you'd just make the block's last expression the desired result, since `let` already returns its last line.
- What's the idiomatic way to early-return when a nullable is null?Use the Elvis operator: `val x = maybe ?: return` (optionally `?: return defaultValue`), which exits when the value is null.
return in a let is a fire alarm for the whole building, not a 'leave this room' button.
saying these in an interview costs you the question
- Saying `return` only exits the `let` block
- Using `?.let { return }` as a null-guard (inverted logic)
- Not knowing scope functions are inline
- Claiming you always need `return@let` to yield a value
- Confusing `apply`/`also` (return receiver) with `let`/`run` (return last expression) in this context