When receiver lambdas are nested, several implicit receivers are in scope at once. How does Kotlin decide which receiver an unqualified call resolves against?
answer
- Innermost matching receiver wins
- Search direction: inner -> outer
- Same name on both: inner shadows outer
- Missing on inner -> falls through to outer
- Qualify this@Label to reach an outer receiver
basics
~10 sThe closest (innermost) receiver wins. Kotlin looks at the nearest enclosing receiver first; if it has a matching member, that's used. Only if it doesn't does it look further out.
solid answer
~40 sNested receiver lambdas stack implicit receivers like scopes. For an unqualified call, the compiler searches from the innermost receiver outward and picks the first one that has a matching member (by name and applicable signature). The innermost matching this shadows outer ones. So inside html { body { } }, an unqualified call first checks Body, then Html. If a name exists on both, the inner one is chosen and the outer is hidden. To deliberately target an outer receiver you must qualify with this@Label (e.g. this@html). Resolution is by member presence and applicability, not just by name — if the innermost receiver lacks the member entirely, the search continues outward. @DslMarker can turn this implicit shadowing into a hard compile error to prevent accidental outer-receiver calls.
code
kotlin · 13 linesclass Html { fun meta() = "html-meta" ; fun title() = "html-title" }
class Body { fun title() = "body-title" }
fun html(b: Html.() -> Unit) = Html().b()
fun Html.body(b: Body.() -> Unit) = Body().b()
html {
body {
title() // "body-title" (inner Body shadows Html)
meta() // "html-meta" (only Html has it)
this@html.title() // "html-title" (explicit outer)
}
}go deeper
Knows the closest receiver is preferred over outer ones.
States innermost-wins precisely, including fall-through when the inner receiver lacks the member.
Explains it as applicability-driven shadowing and ties it to why @DslMarker and this@Label exist.
Reasons about API design: how shadowing risk shapes naming, marker scopes, and DSL ergonomics across deeply nested builders.
## Receivers stack like scopes Each receiver lambda you enter pushes a new implicit receiver onto an inward-growing stack. Inside ```kotlin html { // receiver #1: Html (outer) body { // receiver #2: Body (inner) // both Html and Body are in scope here } } ``` both `Html` and `Body` are available, but they are ordered: **innermost first**. ## The resolution rule: innermost matching receiver wins For an **unqualified** call `foo()`, the compiler walks the implicit receivers from the **innermost outward** and selects the **first receiver that has an applicable member** named `foo`. - If only the outer receiver has `foo`, the inner is skipped and the outer is used — search continues until a match is found. - If **both** have `foo`, the **inner one wins** and shadows the outer. The outer member is not silently merged; it's simply hidden. This is *member presence + applicability* driven, not pure lexical nearness: an inner receiver that lacks the member does not block the outer one. ```kotlin class Outer { fun shared() = "outer" ; fun onlyOuter() = "O" } class Inner { fun shared() = "inner" } fun outer(b: Outer.() -> Unit) = Outer().b() fun Outer.inner(b: Inner.() -> Unit) = Inner().b() outer { inner { shared() // "inner" -> innermost Inner.shared wins onlyOuter() // "O" -> Inner has none, falls out to Outer } } ``` ## Reaching an outer receiver on purpose Use **qualified this** with a label: `[email protected]()` selects the `Outer` receiver explicitly. The label is the name of the function/class that introduced that receiver (`this@html`, `this@Body`, etc.). See the dedicated disambiguation question. ## Guarding the footgun: @DslMarker Because innermost-wins silently hides outer members, calling an outer member by accident is a classic DSL bug. Annotating the DSL marker with `@DslMarker` makes the compiler **reject** an unqualified call that would resolve to an outer receiver of the same marker scope — you must then qualify with `this@...`. (That mechanism is the sibling leaf; here the point is *why* it exists: the innermost-wins shadowing.) ## Mental model - Receivers = a scope stack growing inward. - Unqualified call = search innermost → outermost for an applicable member. - Same name on inner + outer = inner shadows outer. - Want outer = qualify with `this@Label`.
- If the innermost receiver doesn't have the called member, does compilation fail?No — the compiler keeps searching outward and uses the first outer receiver that has an applicable member. It only fails if no receiver in scope provides one.
- Does overload resolution consider all receivers together or one at a time?Receivers are tried in innermost-to-outermost priority; the first receiver yielding an applicable candidate determines the binding, so an inner match prevents outer candidates from being considered.
Like variable shadowing in nested blocks: the nearest declaration in scope hides the one further out.
saying these in an interview costs you the question
- Saying the outermost receiver wins
- Claiming all matching members merge into one overload set across receivers
- Thinking nesting causes an automatic compile error without @DslMarker
- Believing nearness is purely lexical and ignores whether the member exists
- Not knowing this@Label exists to reach outer receivers