When multiple implicit receivers/contexts are in scope and several could resolve the same unqualified call, how does Kotlin decide, and how do you disambiguate?
answer
- receivers form a stack; innermost-first resolution
- nearer shadows farther = no error; equal-priority match = error
- disambiguate with this@with / this@ClassName labels
- context params referenced by name can't be shadowed
- @DslMarker removes the outer same-marked receiver from candidates
basics
~20 sKotlin searches receivers from the innermost scope outward and uses the closest one that has a matching member; a nearer receiver shadows farther ones. If a single scope offers two equally valid candidates it's an ambiguity error. You disambiguate with labeled this (this@outer) or by qualifying the call.
solid answer
~50 sImplicit receivers form a stack ordered by lexical nesting: the innermost receiver/context is tried first, then outward. For an unqualified call, the compiler picks the nearest receiver that has an applicable member; nearer ones shadow farther ones (no error — just shadowing). An error arises only when two candidates sit at the same priority and both apply, or when an extension and a context member tie. You resolve shadowing by qualifying with a labeled this — this@with, this@apply, this@ClassName — to name the exact receiver, or by calling the member explicitly on a named reference (req.path). For context parameters specifically, you reference the context by its declared name (logger.info(...)) which sidesteps implicit-receiver ambiguity entirely. @DslMarker is the structural tool that prevents accidentally reaching outer DSL receivers, but disambiguation of an intended outer call is still done via labels.
code
kotlin · 9 linesclass A { fun who() = "A" }
class B { fun who() = "B" }
fun demo(a: A) = with(a) { // receiver A
B().run { // receiver B (innermost)
println(who()) // "B" — nearest wins
println(this@with.who()) // "A" — label reaches outer
}
}go deeper
Knows the innermost wins but may not know labels or when a true ambiguity error occurs.
Uses this@label to reach an outer receiver and recognizes shadowing vs. error.
Explains the receiver stack precisely, when ambiguity errors fire, and how DslMarker and named contexts change the candidate set.
Sets DSL design conventions (marker usage, nesting limits, named refs) so resolution stays predictable across a large codebase.
## The receiver stack Inside nested receiver lambdas there can be several **implicit receivers** simultaneously. Kotlin treats them as a stack ordered by **lexical proximity**: the innermost lambda's receiver is at the top, the enclosing one below it, and so on, down to the enclosing class's `this`. For an **unqualified** call `foo()` or property `bar`, the compiler walks the stack **innermost-first** and binds to the **nearest** receiver that has an applicable member. A nearer receiver therefore **shadows** a farther one — this is normal and is *not* an error. ```kotlin class Html { fun text(s: String) { } } class Body { fun text(s: String) { } } fun page(html: Html) = with(html) { // outer receiver: Html Body().apply { // inner receiver: Body text("hi") // Body.text — innermost wins (shadows Html.text) [email protected]("x") // Html.text — reached via label } } ``` ## When it's actually ambiguous A real **ambiguity error** happens when two candidates are at the **same** resolution priority and both apply — e.g. two extension functions imported with equal specificity, or a member vs. an equally-applicable context member that the rules can't order. The compiler then refuses to guess and you must qualify. ## Disambiguating 1. **Labeled `this`** — name the receiver explicitly: - `this@with`, `this@run`, `this@apply` (the scope-function name) - `this@ClassName` for an enclosing class - a custom lambda label: `outer@ { ... this@outer ... }` 2. **Explicit reference** — give the object a name (`also { req -> req.path }`, or `val b = Body(); b.text(...)`) so there is no implicit lookup at all. 3. **Context parameters by name** — with `context(logger: Logger)` you write `logger.info(...)`; the named reference cannot be shadowed by another implicit receiver. ## Interaction with @DslMarker `@DslMarker` changes the *rules*: when two implicit receivers are annotated with the **same** DSL-marker annotation, the **outer** one is **excluded** from implicit resolution — so an unqualified call can only hit the innermost. This prevents accidentally calling an outer builder's method from a nested block. It does **not** forbid the intended outer call: you can still reach it with the labeled `this@outer`. (That marker mechanism is the sibling 'DslMarker Scope Control' leaf; here the point is it removes the outer receiver from the candidate set.) ## Practical guidance - Keep nesting shallow; deep receiver stacks make resolution hard to reason about. - Prefer named references (`it`/`also`/let) when clarity matters more than brevity. - Use labels deliberately rather than relying on shadowing you have to reverse-engineer. - For ambient services, context parameters referenced by name avoid the whole shadowing question.
- Is innermost-shadows-outer a compile error you must fix?No — shadowing is legal and silent. It only becomes an error when two candidates tie at the same priority, or when you intended the outer member but the inner one shadowed it (a logic bug, not a compile error).
- How does @DslMarker change this resolution?If the outer and inner receivers carry the same DslMarker annotation, the outer is dropped from implicit candidates, so an unqualified call can only resolve to the inner one; the outer is still reachable via this@label.
Like variable shadowing in nested blocks: the closest declaration wins, and you reach the outer one only by a qualified name.
saying these in an interview costs you the question
- Claims any name clash between receivers is automatically a compile error
- Doesn't know labeled this (this@with) for disambiguation
- Thinks resolution prefers the outermost or the most specific type regardless of nesting
- Confuses @DslMarker (excludes outer receiver) with general shadowing
- Believes context parameters participate in implicit shadowing the same way receivers do