skip to content

What problems arise from nesting receiver-based scope functions (apply/run/with), and how do this/it choices and labels help you avoid them?

level: seniorimportance: should knowfreq 42%

answer

  1. Nested apply/run = multiple implicit this
  2. inner this shadows outer this
  3. fragile: adding a member flips resolution
  4. fix: use it + rename, or this@label
  5. @DslMarker guards outer-receiver access in DSLs

basics

~20 s

Nesting blocks that all use 'this' makes it unclear which object a bare property refers to, and inner this hides outer this. Prefer 'it'-based functions when nesting, name the parameter, or qualify this with a label.

solid answer

~40 s

When you nest apply/run/with, each opens a new implicit this receiver. A bare member call resolves to the nearest receiver that has it, so the inner this shadows the outer — a property added to the inner type later can silently change resolution, and identical member names become ambiguous to readers. Fixes: (1) switch the inner (or outer) scope to an it-based function (let/also) and rename the parameter (also { node -> }) so each object has a distinct name; (2) use a qualified this with a label — this@Outer or this@apply — to disambiguate; (3) avoid deep nesting entirely by extracting a function. The receiver model is powerful for DSLs (with @DslMarker restricting outer-receiver access) but error-prone in ad-hoc nesting.

code

kotlin · 18 lines
kotlin
class Form { var title = "" }
class Field { var title = "" }

// Ambiguous with nested apply:
Form().apply {
    Field().apply {
        title = "field"        // hits Field.title (inner shadows)
        this@apply.title = "form" // qualified label reaches outer
    }
}

// Clear with also + names:
Form().also { form ->
    Field().also { field ->
        field.title = "field"
        form.title = "form"
    }
}

go deeper

for a junior

Recognizes that bare names refer to the object inside an apply/run block.

for a middle

Knows inner this shadows outer this and can rename via it/also to fix it.

for a senior

Explains resolution order, the refactoring fragility, and uses this@label labels correctly.

for a principal

Connects the receiver model to DSL design and @DslMarker, and sets guidelines limiting receiver nesting in code review.

## The implicit-receiver model Receiver-based scope functions (`apply`, `run`, `with`) take a *function literal with receiver* `T.() -> R`. Inside, the object is the **implicit receiver** `this`, so bare names like `name` mean `this.name`. When you nest these blocks, you create **multiple implicit receivers** in scope simultaneously. ## Problem 1 — receiver shadowing Name resolution for a bare member walks from the **innermost** receiver outward and stops at the first one that declares it. So the inner `this` shadows the outer: ```kotlin class Outer { var status = "outer" } class Inner { var status = "inner" } Outer().apply { // this = Outer Inner().apply { // this = Inner (shadows Outer) status = "changed" // sets Inner.status — NOT Outer.status! } } ``` The bare `status` silently targets `Inner`. Worse, this is **fragile**: if `Inner` *didn't* have `status` today it would resolve to `Outer`, but adding a `status` to `Inner` later flips the meaning with no compile error — a refactoring hazard. ## Problem 2 — readability / ambiguity A reader can't tell which object a bare member belongs to without knowing every receiver type. Deeply nested `apply`/`run` blocks become opaque. ## Fix A — prefer `it` and rename when nesting Use `let`/`also` and give each object a distinct name: ```kotlin Outer().also { outer -> Inner().also { inner -> inner.status = "inner changed" outer.status = "outer changed" // unambiguous } } ``` Explicit names remove all ambiguity. This is the most common practical fix. ## Fix B — qualified `this` with labels Every receiver block introduces an implicit label (the function name) and class scopes have `this@ClassName`. Disambiguate explicitly: ```kotlin Outer().apply { // implicit label this@apply (Outer) Inner().apply { this.status = "inner" // inner Inner [email protected] = "outer" // outer Outer's apply receiver } } ``` Within a class method, `this@Outer` reaches the enclosing class instance over a scope-function receiver. ## Fix C — don't nest; extract The cleanest fix is often to pull the inner configuration into a named function so there's only one receiver per scope. ## Where receivers are intentionally good: DSLs and @DslMarker The same multi-receiver model powers type-safe builders (e.g. Kotlin's `buildString`, HTML DSLs). To prevent accidental access to an **outer** receiver from an inner DSL block, Kotlin offers the `@DslMarker` annotation: annotate the DSL's receiver types, and the compiler forbids calling an outer receiver's members implicitly from an inner block, forcing a qualified `this@Outer`. This turns the shadowing footgun into a compile-time guardrail. So: receivers are excellent for designed DSLs, risky for ad-hoc nested scope functions.

  • Why is shadowing especially dangerous during later refactors?
    A bare member resolving to an outer receiver can silently re-bind to an inner one if that inner type gains a member with the same name — no compile error, changed behavior.
  • How does @DslMarker prevent this in DSLs?
    It makes implicit calls to an outer marked receiver a compile error inside an inner block, forcing an explicit this@Outer and eliminating accidental outer access.

Nested 'this' blocks are like two people both named 'this' in the room — say 'this@Outer' or give them real names (it) to avoid talking to the wrong one.

saying these in an interview costs you the question

  • Claiming bare members always resolve to the outermost receiver
  • Not knowing this@label exists
  • Deeply nesting apply/run without disambiguation
  • Unaware of @DslMarker's role
  • Thinking it-based functions have the same shadowing issue (they use named parameters)

context