The Safe Call Operator (?.)
The safe call operator ?. is how you access a property or call a method on a value that might be null without risking a NullPointerException. Instead of crashing, an expression like user?.name simply evaluates to null if user is null, and to the actual property value otherwise. It is the single most-used tool in Kotlin’s null-safety system, and understanding it fully unlocks the rest of the language’s null handling.
Overview / How it works
In Kotlin, every type is either non-null (String) or nullable (String?). The compiler will not let you call a method or read a property directly on a nullable type, because that call might land on null at runtime and blow up. Given val name: String? = getName(), writing name.length is a compile error — the compiler refuses to build the program, catching the bug before it ever ships.
The safe call operator is the compiler-approved way past this restriction. name?.length tells Kotlin: “if name is not null, read .length on it; if it is null, don’t call anything — just produce null instead.” Under the hood, the compiler generates the equivalent of a null check followed by a conditional call: roughly if (name != null) name.length else null, except the check happens once and the whole expression is evaluated safely in one step. Crucially, the type of a safe call expression is always nullable: name?.length has type Int?, even though length itself is a non-null Int on String. The nullability of the receiver “infects” the result, and the compiler tracks this so you can’t accidentally treat the result as guaranteed non-null further down the line.
This matters because it moves an entire class of bugs — the dreaded NPE — from a runtime crash discovered by a user to a compile-time error discovered by you, the developer, before the code ever runs.
Syntax
receiver?.member // safe property access
receiver?.method() // safe method call
a?.b?.c // chained safe calls
| Part | Meaning |
|---|---|
receiver |
An expression of a nullable type (T?) |
?. |
The safe call operator: only proceeds if receiver is non-null |
member / method() |
The property or function to access, only evaluated when the receiver isn’t null |
| Result type | Always nullable — e.g. String?.length gives Int?, not Int |
Examples
Example 1: A basic safe call
fun main() {
val name: String? = null
val length = name?.length
println(length)
}
Output:
null
Because name is null, the safe call never actually invokes .length. The whole expression short-circuits to null, which is assigned to length (inferred as Int?). Printing a null Int? prints the literal text null.
Example 2: Safe calls on nested objects
data class Address(val city: String?)
data class Person(val name: String, val address: Address?)
fun main() {
val person1 = Person("Alice", Address("Seattle"))
val person2 = Person("Bob", null)
println(person1.address?.city)
println(person2.address?.city)
}
Output:
Seattle
null
Address is a data class, so Kotlin auto-generates its equals, hashCode, toString, and copy. person1.address is non-null, so ?.city reads straight through to "Seattle". person2.address is null, so the safe call skips the property read entirely and yields null — without touching .city at all, and without throwing.
Example 3: Chaining safe calls with the elvis operator
fun main() {
val emails: Map = mapOf(
"alice" to "alice@example.com",
"bob" to null
)
val users = listOf("alice", "bob", "carol")
for (user in users) {
val email = emails[user]
val domain = email?.substringAfter("@")?.uppercase()
println("$user -> ${domain ?: "NO EMAIL"}")
}
}
Output:
alice -> EXAMPLE.COM
bob -> NO EMAIL
carol -> NO EMAIL
This combines several null-safety tools at once. emails[user] (a Map lookup) returns String? because the key might be absent — for "bob" the stored value is literally null, and for "carol" the key isn’t in the map at all, so the lookup itself returns null. Either way, email?.substringAfter("@")?.uppercase() chains two safe calls: if email is null, neither substringAfter nor uppercase ever runs, and domain becomes null. The ?: (elvis) operator then supplies a fallback string for display.
How it works step by step
For a chain like a?.b?.c, Kotlin evaluates left to right and stops at the first null it finds:
- Evaluate
a. If it isnull, the entire expression immediately evaluates tonull—bandcare never touched. - If
ais non-null, evaluatea.bnormally. - If the result of
a.bisnull, the expression again short-circuits tonullandcis never evaluated. - If
a.bis non-null, evaluate(a.b).cand that becomes the final result.
This short-circuiting is important for side effects: in a?.doSomething()?.doSomethingElse(), if a is null, doSomething() genuinely never executes — it’s not that its return value is discarded, it’s that the call itself is skipped.
Common Mistakes
Mistake 1: Reaching for !! instead of ?.
New Kotlin developers often “fix” a compile error by slapping !! (the not-null assertion operator) onto a nullable value just to make it compile. This throws away the compiler’s safety net and crashes at runtime the moment the value actually is null.
fun main() {
val name: String? = null
val length = name!!.length
println(length)
}
Output:
Throws a NullPointerException at runtime (Kotlin's !! operator asserts non-null and crashes immediately when the value is actually null).
The fix is to use ?. together with a fallback via the elvis operator, so a null value is handled gracefully instead of crashing:
fun main() {
val name: String? = null
val length = name?.length ?: 0
println(length)
}
Output:
0
Mistake 2: Forgetting the result of a safe call is still nullable
A safe call always produces a nullable type, even if the property itself is non-null. Assigning that result directly to a non-nullable val is a compile error, not a runtime surprise — but it trips people up because the underlying property (length) is an Int, not an Int?.
val name: String? = "Kotlin"
val length: Int = name?.length // compile error: type mismatch, required Int, found Int?
You must either keep the target type nullable or supply a default with ?::
val name: String? = "Kotlin"
val length: Int = name?.length ?: 0
println(length)
Output:
6
Best Practices
- Prefer
?.combined with?:(elvis) or safe-call chaining over!!everywhere except the rare case where a null value truly indicates a programming bug you want to crash loudly on. - Use
value?.let { ... }when you want to run a whole block of code only when a value is non-null, instead of writing a manualif (value != null)check:val name: String? = "Kotlin" name?.let { println("Name has ${it.length} characters") } - Keep safe-call chains short (two or three
?.in a row is fine); a long chain likea?.b?.c?.d?.eusually signals that an intermediate function or a restructured data model would be clearer. - Remember that the result of any safe call is nullable, so plan the type of the variable you’re assigning it to accordingly, or terminate the chain with
?:to unwrap it to a concrete default. - Don’t confuse
?.(safe call) with?:(elvis, provides a default) or!!(asserts non-null and crashes) — each solves a different problem.
Practice Exercises
- Write a function
fun shout(text: String?): Stringthat returnstextuppercased with an exclamation mark appended (e.g."hi"→"HI!"), or the literal string"..."iftextis null. Use?.and?:, not anifstatement. - Given
data class Book(val title: String, val author: Author?)anddata class Author(val name: String?), write code that prints an author’s name for aBook, or"Unknown author"if either the author or the author’s name is missing. (Hint: you’ll need to chain two safe calls.) - Given a
val scores: Map, write a loop that prints each name together with their score, or= mapOf("amy" to 90, "raj" to null) "no score"if the value is null or the key is missing entirely.
Summary
?.safely accesses a property or calls a method on a nullable receiver, evaluating tonullinstead of throwing when the receiver isnull.- The result of a safe call is always a nullable type, regardless of whether the underlying member is non-null.
- Chains of
?.short-circuit at the firstnull, so later calls in the chain are never evaluated once a null is hit. - Pair
?.with?:to supply a default value, or with?.let { ... }to run a block of code only on non-null values. - Avoid
!!in place of?.— it discards the compiler’s safety guarantee and turns a preventable bug into a runtime crash.
