Kotlin

Multiple variable let in Kotlin

27 September 2026 · 8 min read

Multiple variable let in Kotlin

Kotlin, a modern, concise, and pragmatic programming language, offers a suite of powerful scope functions designed to make code more readable and expressive. Among these, the let function stands out for its utility in executing a code block on a non-null object, preventing potential NullPointerExceptions. While let inherently operates on a single receiver, the concept of handling multiple variable let in Kotlin often arises when developers need to process several related pieces of data, especially if some might be nullable. This article delves into how Kotlin empowers you to manage multiple variables effectively within the context of let, primarily leveraging destructuring declarations and smart casting, to write cleaner, safer, and more idiomatic code.

Understanding Kotlin’s let Scope Function

The let function in Kotlin is a versatile tool that allows you to perform operations on an object if it’s not null, or to introduce a new variable for a limited scope. When you call let on an object, it becomes the context object within the lambda expression, accessible via the implicit it keyword or a custom name you define. This mechanism is incredibly useful for null safety, ensuring that you only proceed with operations on an object once its non-null status is confirmed.

For instance, consider a scenario where you retrieve user data that might be null. Using let, you can safely process this data. The function returns the result of the lambda expression, providing flexibility for chaining operations or assigning results. This approach significantly reduces boilerplate code compared to traditional null checks, making your intentions clearer and your code more robust against runtime errors.

let is part of Kotlin’s broader suite of scope functions, including apply, run, with, and also. Each has its specific use cases, but let is particularly favored when you need to perform actions on a non-null object and return a different type, or simply use the object in an expression. It embodies a functional programming style, promoting immutability and reducing side effects when used correctly.

The Synergy: Destructuring Declarations and let

While let itself works on a single receiver, the idea of processing multiple variable let in Kotlin becomes powerful when combined with destructuring declarations. Destructuring allows you to “unpack” an object into multiple variables, making it seem as though let is handling multiple variables directly. This is particularly effective for data classes or objects that implement the componentN() functions, enabling you to extract properties directly into named variables.

Featured Snippet: To effectively handle multiple related variables with Kotlin’s let function, you typically employ destructuring declarations. When a nullable object containing multiple properties is passed into a let block, you can immediately unpack its components into distinct variables, such as myObject?.let { (propertyA, propertyB) -> / use propertyA and propertyB here / }. This pattern ensures that the code inside the block only executes if myObject is not null, and provides direct access to its constituent parts for clear, concise, and null-safe operations.

Consider a scenario where you have a User data class with name, email, and age. If a User object might be nullable, you can use let to ensure it’s not null, then immediately destructure it:

data class User(val name: String, val email: String, val age: Int?) fun processUser(user: User?) { user?.let { (name, email, age) -> println("Processing user: Name=$name, Email=$email") age?.let { println("User age: $it") } ?: println("Age not provided.") } ?: println("User data is null. Cannot process.") } 

This approach elegantly combines null safety with convenient access to individual properties, making the code highly readable and efficient. It’s a prime example of idiomatic Kotlin that embraces both safety and conciseness, significantly improving the clarity of operations on potentially complex data structures.

Practical Applications and Advanced Patterns

The combination of let and destructuring declarations unlocks numerous practical applications in Kotlin development, particularly when dealing with data validation, UI updates, or complex business logic where multiple parameters might be involved and some could be optional. This pattern is invaluable for writing robust and maintainable code.

One common use case involves processing form inputs where several fields need to be present and valid before an action can be performed. Instead of multiple nested if (x != null) checks, you can streamline the logic:

  1. Define a data class or a type alias to group related parameters, even if temporarily.
  2. Create an instance of this type, possibly with nullable fields.
  3. Apply let to the main object, ensuring it’s not null.
  4. Destructure the object within the let block to access its components.
  5. Perform validation or operations on the destructured variables.

For scenarios requiring more complex null checks involving multiple independent nullable variables, a single let call isn’t sufficient. Here, you might use a combination of if conditions or chain multiple let calls, or even use a run block for better readability. For instance, to ensure three independent variables are all non-null:

fun processOrder(item: String?, quantity: Int?, price: Double?) { if (item != null && quantity != null && price != null) { // All non-null, process order val totalPrice = quantity  price println("Order: $item, Quantity: $quantity, Total Price: $totalPrice") } else { println("Missing order details.") } } 

While the if check is straightforward, for more complex logic or transformations, chaining let or using run can be more expressive, especially when dealing with transformations. This pattern simplifies code that would otherwise be cluttered with numerous null checks. According to a [A slight change to have the function receive the list of items and do the same operations:

fun <T: Any, R: Any> whenAllNotNull(vararg options: T?, block: (List<T>)->R) { if (options.all { it != null }) { block(options.filterNotNull()) // or do unsafe cast to non null collection } } fun <T: Any, R: Any> whenAnyNotNull(vararg options: T?, block: (List<T>)->R) { if (options.any { it != null }) { block(options.filterNotNull()) } } 

Example usage:

whenAllNotNull("something", "else", "matters") { println(it.joinToString(" ")) } // output "something else matters" 

These variations could be changed to have return values like let().

Use the first non-null item (Coalesce)

Similar to a SQL Coalesce function, return the first non null item. Two flavours of the function:

fun <T: Any> coalesce(vararg options: T?): T? = options.firstOrNull { it != null } fun <T: Any> Collection<T?>.coalesce(): T? = this.firstOrNull { it != null } 

Example usage:

coalesce(null, "something", null, "matters")?.let { it.length } // result is 9, length of "something" listOf(null, "something", null, "matters").coalesce()?.let { it.length } // result is 9, length of "something" 

Other variations

…There are other variations, but with more of a specification this could be narrowed down.](<https://blog.jetbrains.com/kotlin/2021/01/kotlin-census-2020-results Question & Answer :

Is there any way to chain multiple lets for multiple nullable variables in kotlin?

fun example(first: String?, second: String?) { first?.let { second?.let { // Do something just if both are != null } } } 

I mean, something like this:

fun example(first: String?, second: String?) { first?.let && second?.let { // Do something just if both are != null } } 

Here are a few variations, depending on what style you will want to use, if you have everything of same or different types, and if the list unknown number of items…

Mixed types, all must not be null to calculate a new value

For mixed types you could build a series of functions for each parameter count that may look silly, but work nicely for mixed types:

inline fun <T1: Any, T2: Any, R: Any> safeLet(p1: T1?, p2: T2?, block: (T1, T2)->R?): R? { return if (p1 != null && p2 != null) block(p1, p2) else null } inline fun <T1: Any, T2: Any, T3: Any, R: Any> safeLet(p1: T1?, p2: T2?, p3: T3?, block: (T1, T2, T3)->R?): R? { return if (p1 != null && p2 != null && p3 != null) block(p1, p2, p3) else null } inline fun <T1: Any, T2: Any, T3: Any, T4: Any, R: Any> safeLet(p1: T1?, p2: T2?, p3: T3?, p4: T4?, block: (T1, T2, T3, T4)->R?): R? { return if (p1 != null && p2 != null && p3 != null && p4 != null) block(p1, p2, p3, p4) else null } inline fun <T1: Any, T2: Any, T3: Any, T4: Any, T5: Any, R: Any> safeLet(p1: T1?, p2: T2?, p3: T3?, p4: T4?, p5: T5?, block: (T1, T2, T3, T4, T5)->R?): R? { return if (p1 != null && p2 != null && p3 != null && p4 != null && p5 != null) block(p1, p2, p3, p4, p5) else null } // …keep going up to the parameter count you care about 

Example usage:

val risk = safeLet(person.name, person.age) { name, age -> // do something } 

Execute block of code when list has no null items

Two flavours here, first to execute block of code when a list has all non null items, and second to do the same when a list has at least one not null item. Both cases pass a list of non null items to the block of code:

Functions:

fun <T: Any, R: Any> Collection<T?>.whenAllNotNull(block: (List)->R) { if (this.all { it != null }) { block(this.filterNotNull()) // or do unsafe cast to non null collection } } fun <T: Any, R: Any> Collection<T?>.whenAnyNotNull(block: (List)->R) { if (this.any { it != null }) { block(this.filterNotNull()) } } 

Example usage:

listOf(>)