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 is null, the entire expression immediately evaluates to nullb and c are never touched.
  • If a is non-null, evaluate a.b normally.
  • If the result of a.b is null, the expression again short-circuits to null and c is never evaluated.
  • If a.b is non-null, evaluate (a.b).c and 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 manual if (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 like a?.b?.c?.d?.e usually 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?): String that returns text uppercased with an exclamation mark appended (e.g. "hi""HI!"), or the literal string "..." if text is null. Use ?. and ?:, not an if statement.
  • Given data class Book(val title: String, val author: Author?) and data class Author(val name: String?), write code that prints an author’s name for a Book, 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 = mapOf("amy" to 90, "raj" to null), write a loop that prints each name together with their score, or "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 to null instead of throwing when the receiver is null.
  • 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 first null, 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.