skip to content

How does chaining safe calls like a?.b?.c work, and what is the result type? What happens if b is null in the middle?

level: middleimportance: must knowfreq 80%

answer

  1. left-to-right, stop at first null
  2. every nullable link needs its own ?.
  3. plain . is fine after a non-null link
  4. result type = last member type, nullable
  5. later side effects skipped on short-circuit

basics

~20 s

You can stack ?. through a chain. If any link is null, the whole chain stops and becomes null. The final result is nullable. So a?.b?.c is null if a, a.b, or a.b.c is null.

solid answer

~40 s

Chained safe calls evaluate left to right and short-circuit at the first null. In a?.b?.c, Kotlin evaluates a; if null, the entire expression is null and nothing further runs. Otherwise it reads a.b; if that is null, it stops and yields null; otherwise it reads .c. The overall type is the type of c made nullable, e.g. C?. Each ?. is its own guard, so you don't need a ?. after a link that is already non-null — but you DO need it on every link whose declared type is nullable. A common mistake is writing one ?. and a plain . later (a?.b.c), which fails to compile if b is nullable, because .c on a nullable b isn't allowed.

code

kotlin · 3 lines
kotlin
val name: String? = country?.capital?.mayor?.name
// null if country, capital, or mayor is null
// .name uses plain dot only if mayor.name is non-null typed

go deeper

for a junior

Knows a null anywhere in the chain makes the result null.

for a middle

Explains per-link guards, short-circuit of side effects, and result nullability.

for a senior

Articulates when plain . is legal mid-chain and how type inference drives where ?. is mandatory.

for a principal

Weighs deep safe-call chains vs. modeling absence explicitly (e.g., restructuring types) for readability and maintainability.

## Chaining safe calls Safe calls compose. A chain like `a?.b?.c?.d` is read **left to right**, and each `?.` is an independent guard. The chain **short-circuits**: at the first `null` receiver, evaluation stops and the whole expression becomes `null`. ```kotlin class Country(val capital: City?) class City(val mayor: Person?) class Person(val name: String) val c: Country? = null val name: String? = c?.capital?.mayor?.name // null, stops at c ``` ### Where do you need `?.`? You need `?.` on **every link whose static type is nullable**. If a property is declared non-null, a plain `.` is fine after it: ```kotlin // capital: City? (nullable), mayor: Person? (nullable), name: String (non-null) c?.capital?.mayor?.name // ^^^ ^^^ each nullable link needs ?. // .name uses plain . because Person.name is non-null ``` Writing `c?.capital.mayor` would **not compile**: `capital` is `City?`, so `.mayor` on it is illegal — you must write `?.mayor`. ### Result type The whole chain's type is the **last member's type, made nullable**. `name` is `String`, but `c?.capital?.mayor?.name` is `String?` because any earlier null collapses the result to null. ### Short-circuit detail Once a `null` is hit, **no later side effects run**. If links were function calls (`a?.foo()?.bar()`), `bar()` is skipped entirely when `foo()` returns null. This makes chains safe but also means you can't rely on later calls executing. ### Collapsing back to non-null Chains are usually finished with Elvis (`?: default`) or `?.let { ... }` to turn the `T?` into a usable non-null value — but those are separate operators layered on top of the chain.

  • Does a?.foo()?.bar() call bar() if foo() returns null?
    No. The chain short-circuits at the null returned by foo(); bar() is never invoked.
  • Why might a?.capital.mayor fail to compile?
    If capital is nullable, you cannot use a plain . to access .mayor on it; you must use ?.mayor.

saying these in an interview costs you the question

  • Saying the chain throws if a middle link is null
  • Claiming you only need one ?. at the start of the chain
  • Thinking later calls still run after a null short-circuit
  • Saying the chain result is non-null
  • Confusing where plain . is allowed vs. where ?. is required

context