Avoid Null Pointer Errors

Problem

While Kotlin’s null safety prevents NullPointerException, you still need elegant ways to handle null values in real applications. Explicit null checks can become verbose. Elvis and safe call operators provide concise, expressive patterns for null handling.

This guide shows advanced techniques for handling nulls with Kotlin’s operators.

Safe Call Operator (?.) Patterns

Chaining Safe Calls

Chain multiple safe calls to navigate nullable object graphs.

data class Address(val street: String, val city: String, val country: String)
data class Profile(val name: String, val address: Address?)
data class User(val id: String, val profile: Profile?)

// ✅ Graceful null handling in chains
fun getUserCountry(user: User?): String? {
  return user?.profile?.address?.country
}

// ✅ If any part is null, returns null
val user1 = User("1", Profile("Alice", Address("Main St", "NYC", "USA")))
println(getUserCountry(user1))  // "USA"

val user2 = User("2", Profile("Bob", null))
println(getUserCountry(user2))  // null

val user3 = User("3", null)
println(getUserCountry(user3))  // null

How it works: Safe call chain short-circuits at first null, returning null immediately.

Safe Calls with Functions

Apply safe calls to method invocations.

// ✅ Safe method calls
val text: String? = getUserInput()
val uppercase: String? = text?.trim()?.uppercase()

// ✅ With multiple operations
fun processEmail(email: String?): String? {
  return email
    ?.trim()
    ?.lowercase()
    ?.takeIf { it.contains("@") }
}

// ✅ Safe calls prevent errors
processEmail("  USER@EXAMPLE.COM  ")  // "user@example.com"
processEmail("invalid-email")         // null
processEmail(null)                    // null

Pattern: Chain safe calls with transformations for null-safe pipelines.

Safe Calls with Collections

Use safe calls on nullable collections.

// ✅ Nullable collection access
val items: List<String>? = getItems()
val firstItem: String? = items?.firstOrNull()
val itemCount: Int? = items?.size

// ✅ Safe iteration
items?.forEach { item ->
  println(item)
}

// ✅ Map operations
val uppercased: List<String>? = items?.map { it.uppercase() }

// ✅ Filter operations
val filtered: List<String>? = items?.filter { it.startsWith("A") }

Key benefit: Avoid null checks before collection operations.

Elvis Operator (?:) Patterns

Simple Default Values

Provide fallback values with Elvis operator.

// ✅ Basic default
val name: String? = getUsername()
val displayName: String = name ?: "Guest"

// ✅ With safe calls
val city: String = user?.profile?.address?.city ?: "Unknown"

// ✅ Numeric defaults
val timeout: Int = config.getTimeout() ?: 30
val retries: Int = config.getRetries() ?: 3

// ✅ Collection defaults
val items: List<String> = getItems() ?: emptyList()
val users: Set<User> = fetchUsers() ?: emptySet()

Pattern: nullable ?: default - concise alternative to if-else.

Elvis with Expressions

Right side of Elvis can be any expression.

// ✅ Elvis with function call
fun getConfig(key: String): String {
  return loadFromCache(key) ?: loadFromDatabase(key)
}

// ✅ Multiple fallbacks
fun getEmail(user: User?): String {
  return user?.email
    ?: user?.profile?.contactEmail
    ?: user?.profile?.backupEmail
    ?: "no-email@example.com"
}

// ✅ Elvis with calculations
fun calculatePrice(discount: Double?): Double {
  val basePrice = 100.0
  return basePrice - (discount ?: (basePrice * 0.1))  // 10% default discount
}

Use case: Cascading fallback logic.

Elvis with Control Flow

Use Elvis with return, throw, or continue for validation.

// ✅ Early return
fun processUser(user: User?) {
  val validUser = user ?: return
  println("Processing: ${validUser.name}")
}

// ✅ Throw exception
fun requireConfig(config: Config?): Config {
  return config ?: throw IllegalStateException("Config not initialized")
}

// ✅ Custom exception
class ValidationException(message: String) : Exception(message)

fun validateInput(input: String?): String {
  return input?.trim() ?: throw ValidationException("Input required")
}

// ✅ In loops
fun processItems(items: List<Item?>?) {
  items ?: return  // Early exit if list is null

  for (item in items) {
    val validItem = item ?: continue  // Skip null items
    process(validItem)
  }
}

Pattern: Guard clauses with Elvis for concise validation.

Combining Safe Call and Elvis

Complete Null Handling Pattern

Combine both operators for robust null handling.

// ✅ Safe call + Elvis = null-safe with default
val city: String = user?.profile?.address?.city ?: "Unknown"

// ✅ With transformation
val displayEmail: String = user?.email?.lowercase() ?: "no-email@example.com"

// ✅ Multiple operations
fun getUserDisplay(user: User?): String {
  return user?.profile?.name?.trim()?.takeIf { it.isNotEmpty() } ?: "Anonymous"
}

Pattern: nullable?.property?.method() ?: default - most common null handling pattern.

Safe Call with let and Elvis

Combine ?.let with Elvis for advanced patterns.

// ✅ Execute block or provide default
val result: String = user?.let { u ->
  "User: ${u.name}, Email: ${u.email}"
} ?: "No user available"

// ✅ Transform or default
fun formatPrice(price: Double?): String {
  return price?.let { "$${String.format("%.2f", it)}" } ?: "Price unavailable"
}

// ✅ Nested safe operations
fun getFullAddress(user: User?): String {
  return user?.profile?.address?.let { addr ->
    "${addr.street}, ${addr.city}, ${addr.country}"
  } ?: "Address not provided"
}

Use case: Complex transformations with fallbacks.

Advanced Patterns

Null-Safe Assignment Chains

Build objects safely with nullable sources.

data class UserProfile(
  val name: String,
  val email: String,
  val city: String,
  val country: String
)

// ✅ Safe construction
fun buildProfile(user: User?): UserProfile? {
  return user?.let {
    UserProfile(
      name = it.profile?.name ?: return null,
      email = it.email ?: return null,
      city = it.profile?.address?.city ?: "Unknown",
      country = it.profile?.address?.country ?: "Unknown"
    )
  }
}

Pattern: Early return in let blocks for required fields.

Elvis with run or apply

Use scope functions for complex fallback logic.

// ✅ Elvis with run for multi-line default
val config = loadConfig() ?: run {
  logger.warn("Config not found, using defaults")
  createDefaultConfig()
}

// ✅ Elvis with apply for setup
val connection = establishConnection() ?: run {
  logger.error("Connection failed, creating fallback")
  createFallbackConnection()
}.apply {
  setTimeout(5000)
  setRetries(3)
}

Use case: Complex initialization with fallbacks.

Null-Safe Comparison Chains

Use safe calls in comparison logic.

// ✅ Safe comparisons
fun isUserActive(user: User?): Boolean {
  return user?.profile?.status?.equals("active") ?: false
}

// ✅ Safe numeric comparisons
fun hasLowBalance(account: Account?): Boolean {
  return (account?.balance ?: 0.0) < 100.0
}

// ✅ Safe date comparisons
fun isExpired(subscription: Subscription?): Boolean {
  val expiryDate = subscription?.expiryDate ?: return true
  return expiryDate.isBefore(LocalDate.now())
}

Common Pitfalls

Overusing Elvis with Complex Logic

// ❌ Hard to read
val result = user?.profile?.address?.city
  ?: user?.profile?.homeAddress?.city
  ?: user?.businessAddress?.city
  ?: config.defaultCity
  ?: "Unknown"

// ✅ Extract to function
fun getUserCity(user: User?): String {
  user?.profile?.address?.city?.let { return it }
  user?.profile?.homeAddress?.city?.let { return it }
  user?.businessAddress?.city?.let { return it }
  return config.defaultCity ?: "Unknown"
}

Why problematic: Long Elvis chains reduce readability.

Returning Null Instead of Meaningful Default

// ❌ Returns null, forcing callers to handle it
fun getDisplayName(user: User?): String? {
  return user?.profile?.name
}

// ✅ Provide meaningful default
fun getDisplayName(user: User?): String {
  return user?.profile?.name ?: "Guest"
}

Why problematic: Propagating nullability unnecessarily.

Elvis with Side Effects

// ❌ Side effect in Elvis expression
val user = findUser(id) ?: {
  logger.error("User not found")  // ❌ This is a lambda, not called!
  null
}()

// ✅ Use run for side effects
val user = findUser(id) ?: run {
  logger.error("User not found")
  null
}

// ✅ Or separate statements
val user = findUser(id)
if (user == null) {
  logger.error("User not found")
}

Why problematic: Elvis evaluates expressions, not blocks.

Excessive Safe Call Chaining

// ❌ Too deep, hard to debug
val value = obj?.a?.b?.c?.d?.e?.f?.g

// ✅ Break into steps
val value = obj?.a?.b?.c?.let { intermediate ->
  intermediate.d?.e?.f?.g
}

// ✅ Or validate structure
fun getValue(obj: ComplexObject?): String? {
  val a = obj?.a ?: return null
  val b = a.b ?: return null
  val c = b.c ?: return null
  return c.d?.e?.f?.g
}

Why problematic: Deep chains hide which part is null.

Variations

takeIf and takeUnless with Elvis

Conditional null handling with validation.

// ✅ takeIf returns null if predicate fails
val validEmail = email?.takeIf { it.contains("@") } ?: "invalid@example.com"

// ✅ takeUnless returns null if predicate succeeds
val nonEmptyText = text?.takeUnless { it.isEmpty() } ?: "No text"

// ✅ Chain with safe calls
fun validateAge(input: String?): Int {
  return input
    ?.trim()
    ?.toIntOrNull()
    ?.takeIf { it in 0..150 }
    ?: throw IllegalArgumentException("Invalid age")
}

Safe Cast with Elvis

Combine safe casting with fallbacks.

// ✅ Safe cast with default
val number: Int = value as? Int ?: 0

// ✅ With transformation
val text: String = (value as? String)?.uppercase() ?: "NOT A STRING"

// ✅ Multiple type attempts
fun parseValue(value: Any): Number {
  return (value as? Int)
    ?: (value as? Double)
    ?: (value as? Long)
    ?: 0
}

Null-Coalescing with Sequences

Find first non-null value in sequence.

// ✅ First non-null from multiple sources
fun getConfiguration(): Config {
  return loadFromEnvironment()
    ?: loadFromFile()
    ?: loadFromDefaults()
    ?: throw IllegalStateException("No configuration available")
}

// ✅ With sequenceOf
fun findValue(key: String): String? {
  return sequenceOf(
    cache.get(key),
    database.query(key),
    api.fetch(key)
  ).firstOrNull { it != null }
}

Related Patterns

Learn more:

Cookbook recipes:

Last updated