How do I use run, apply, also, and with for cleaner Kotlin code?

In Kotlin, run, apply, also, and with are scope functions. They help make code cleaner by giving you a temporary scope around an object.

The main differences are:

Function Object reference Returns Best used for
apply this The original object Configuring an object
also it The original object Side effects, logging, validation
run this Lambda result Computing a result from an object
with this Lambda result Grouping operations on an object

apply: configure an object

Use apply when you want to initialize or configure an object and then keep using that same object.

val user = User().apply {
    name = "Alice"
    age = 30
    active = true
}

This is cleaner than:

val user = User()
user.name = "Alice"
user.age = 30
user.active = true

apply returns the original object:

val button = Button().apply {
    text = "Save"
    isEnabled = true
}

Use apply for:

  • object setup
  • builder-style initialization
  • setting multiple properties

also: perform side effects

Use also when you want to do something extra with an object without changing the main flow.

val user = repository.findUser(id)
    .also {
        logger.info("Loaded user: ${it.name}")
    }

also uses it and returns the original object.

Another example:

val numbers = mutableListOf<Int>()
    .also {
        println("Created list")
    }

Good use cases:

val file = File("data.txt")
    .also {
        require(it.exists()) { "File does not exist" }
    }

Use also for:

  • logging
  • debugging
  • validation
  • side effects that should not change the returned value

run: compute a result from an object

Use run when you want to execute code using an object and return a computed result.

val description = user.run {
    "$name is $age years old"
}

Here, run returns the last expression:

val length = "Kotlin".run {
    lowercase().length
}

run is useful when you want to avoid repeating the object name:

val isAdult = user.run {
    age >= 18
}

Use run for:

  • computing a value
  • using several properties/methods of an object
  • keeping temporary logic contained

with: group operations on an object

with is similar to run, but it is not called as an extension function.

val result = with(user) {
    "$name is $age years old"
}

This:

with(user) {
    println(name)
    println(age)
}

is cleaner than:

println(user.name)
println(user.age)

Use with when you already have an object and want to perform several operations with it.

val summary = with(order) {
    "Order $id has $itemCount items and costs $total"
}

Quick comparison

apply

val person = Person().apply {
    name = "Alice"
    age = 30
}

Meaning:

Configure this object and return the object.


also

val person = Person("Alice")
    .also {
        println("Created person: $it")
    }

Meaning:

Do something with this object and return the object.


run

val label = person.run {
    "$name ($age)"
}

Meaning:

Use this object to compute and return a result.


with

val label = with(person) {
    "$name ($age)"
}

Meaning:

With this object, compute and return a result.


Practical example

Without scope functions:

val request = Request()
request.url = "https://example.com"
request.method = "GET"
request.headers["Accept"] = "application/json"

logger.info("Created request: $request")

val summary = "Request to ${request.url} using ${request.method}"

With scope functions:

val request = Request().apply {
    url = "https://example.com"
    method = "GET"
    headers["Accept"] = "application/json"
}.also {
    logger.info("Created request: $it")
}

val summary = request.run {
    "Request to $url using $method"
}

Rule of thumb

Use this simple guide:

Need to configure an object?        apply
Need side effects like logging?     also
Need to compute a result?           run
Need to operate on an existing obj? with

Or more specifically:

  • Use apply when the lambda modifies or configures the receiver.
  • Use also when the lambda performs side effects and should not affect the returned value.
  • Use run when the lambda produces a new value.
  • Use with when you want to group operations on an existing object.

One warning: avoid overusing them

Scope functions can make code cleaner, but too many nested scope functions can make code harder to read.

Avoid this:

user.apply {
    address?.run {
        city.also {
            logger.info(it)
        }
    }
}

Prefer clearer code when nesting gets confusing:

val address = user.address ?: return
logger.info(address.city)

In short: scope functions are best when they reduce repetition and make intent obvious.

How do I use let to execute code only when a value is not null in Kotlin?

Use ?.let { ... }.

The safe-call operator ?. makes sure let is called only when the value is not null. Inside the let block, the value is available as a non-null value, usually named it.

val name: String? = "Kotlin"

name?.let {
    println("Name is $it")
    println("Length is ${it.length}")
}

If name is "Kotlin", the block runs.

If name is null, the block is skipped.

You can also give the value a clearer name:

val name: String? = "Kotlin"

name?.let { nonNullName ->
    println("Name is $nonNullName")
    println("Length is ${nonNullName.length}")
}

To handle the null case too, combine it with the Elvis operator ?::

val name: String? = null

name?.let {
    println("Name is $it")
} ?: println("Name was null")

So the general pattern is:

nullableValue?.let { value ->
    // Runs only when nullableValue is not null
}

How do I force unwrap a nullable value with !! in Kotlin (and why should I avoid it)?

In Kotlin, !! is the not-null assertion operator. It forcefully converts a nullable value like String? into a non-null value like String.

val name: String? = getName()

val length = name!!.length

This tells Kotlin:

“Trust me, name is not null.”

If name is actually null at runtime, Kotlin throws a NullPointerException.

val name: String? = null

println(name!!.length) // Throws NullPointerException

Why you should avoid !!

You should avoid !! because it bypasses Kotlin’s null-safety system. Kotlin’s nullable types exist specifically to help prevent null-related crashes, and !! effectively says: “ignore that safety check.”

Problems with !!:

  • It can cause runtime crashes.
  • It hides the fact that a value may be missing.
  • It often makes code less clear and less robust.
  • It usually means null handling should be improved.

Prefer safer alternatives

Use a safe call

val length = name?.length

If name is null, length becomes null.

Use Elvis operator for a default value

val length = name?.length ?: 0

If name is null, length becomes 0.

Use an explicit null check

if (name != null) {
    println(name.length)
} else {
    println("Name is missing")
}

Inside the if, Kotlin smart-casts name to a non-null String.

Use let

name?.let {
    println(it.length)
}

This only runs the block when name is not null.

Fail deliberately with a clearer message

If null truly represents a programmer error, prefer requireNotNull, checkNotNull, or an explicit error message:

val length = requireNotNull(name) { "Name must not be null" }.length

This still fails fast, but the error is much clearer than a generic NullPointerException.

When is !! acceptable?

Use !! only when you are absolutely certain the value cannot be null, and if it is null, that indicates a serious programming error.

Even then, this is usually better:

val user = requireNotNull(findUser(id)) {
    "Expected user with id=$id to exist"
}

Rule of thumb

If you are tempted to write this:

value!!

First ask whether one of these would be better:

value?.someCall()
value ?: defaultValue
if (value != null) { /* use value */ }
requireNotNull(value) { "Helpful error message" }

In most Kotlin code, !! should be rare.

How do I throw an exception when a value is null using ?: throw in Kotlin?

Use Kotlin’s Elvis operator ?: with throw on the right-hand side:

val value: String? = getNullableValue()

val nonNullValue: String = value ?: throw IllegalArgumentException("value must not be null")

Because throw is an expression in Kotlin, it can be used after ?:.

Example

fun printLength(text: String?) {
    val nonNullText = text ?: throw IllegalArgumentException("text must not be null")

    println(nonNullText.length)
}

If text is not null, it is assigned to nonNullText as a non-nullable String.
If text is null, the exception is thrown.

You can also use other exception types:

val id = nullableId ?: throw IllegalStateException("ID was unexpectedly null")

A common choice is:

  • IllegalArgumentException when a function argument is invalid
  • IllegalStateException when the object/program state is invalid

How do I use the Elvis operator to provide default values in Kotlin?

In Kotlin, the Elvis operator ?: provides a fallback value when the expression on its left is null.

val result = nullableValue ?: defaultValue

If nullableValue is not null, result gets that value.
If nullableValue is null, result gets defaultValue.

Example:

val name: String? = null

val displayName = name ?: "Guest"

println(displayName) // Guest

With a non-null value:

val name: String? = "Alice"

val displayName = name ?: "Guest"

println(displayName) // Alice

It’s often used with safe calls:

val name: String? = null

val nameLength = name?.length ?: 0

println(nameLength) // 0

Here, name?.length returns null if name is null, so ?: 0 supplies the default.

You can also use it with functions:

fun getUsername(): String? {
    return null
}

val username = getUsername() ?: "anonymous"

And because throw and return are expressions in Kotlin, they can be used on the right side:

fun printName(name: String?) {
    val actualName = name ?: return
    println(actualName)
}
fun requireName(name: String?) {
    val actualName = name ?: throw IllegalArgumentException("Name is required")
    println(actualName)
}

So the basic pattern is:

val value = somethingNullable ?: fallbackValue

How do I use the safe call operator `?.` in Kotlin?

In Kotlin, the safe call operator ?. lets you access a property or call a function only if the value is not null.

If the value is null, the expression simply returns null instead of throwing a NullPointerException.

val name: String? = null

val length = name?.length

println(length) // null

Here, name is nullable because its type is String?. Since name is null, name?.length does not try to access .length; it returns null.

Basic syntax

nullableValue?.property
nullableValue?.function()

Example:

val user: User? = getUser()

val email = user?.email

If user is not null, email gets user.email.

If user is null, email becomes null.

Chaining safe calls

You can chain multiple safe calls together:

val city = user?.address?.city

This means:

  • if user is null, return null
  • otherwise check address
  • if address is null, return null
  • otherwise return city

Using ?. with a default value

Often, you combine ?. with the Elvis operator ?::

val length = name?.length ?: 0

This means:

  • if name is not null, use name.length
  • if name is null, use 0

Using ?.let

Use ?.let when you want to run code only when a value is not null:

val name: String? = "Kotlin"

name?.let {
    println("Name is $it")
    println("Length is ${it.length}")
}

The block runs only if name is not null.

Safe call on assignment

Safe calls can also be used on the left side of an assignment:

person?.address?.city = "Paris"

If person or address is null, the assignment is skipped.

Summary

val result = nullableValue?.someProperty

Use ?. when:

  • a value might be null
  • you want to avoid NullPointerException
  • returning null is acceptable when the receiver is null

Common pattern:

val result = nullableValue?.someProperty ?: defaultValue

How do I declare nullable variables using `?` in Kotlin?

In Kotlin, you declare a nullable variable by adding ? after the type.

var name: String? = null

This means name can hold either a String value or null.

Examples:

var age: Int? = null
age = 25

val email: String? = "[email protected]"
val phone: String? = null

Without ?, Kotlin does not allow null:

var name: String = null // Error

With ?, you must handle possible null safely:

val name: String? = null

println(name?.length) // Safe call, prints null instead of crashing

You can also provide a default value with the Elvis operator ?::

val name: String? = null
val length = name?.length ?: 0

println(length) // 0

So the basic pattern is:

var variableName: Type? = null

How do I handle null values safely in Kotlin?

Kotlin handles null safety through its type system: a normal type like String cannot be null, while a nullable type like String? can be null.

1. Use nullable types when a value may be null

val name: String = "Alice"   // Cannot be null
val nickname: String? = null // Can be null

If a value is nullable, Kotlin will not let you access it directly without handling the null case.

val length = nickname.length // Compile error

2. Use the safe-call operator ?.

Use ?. to access a property or call a function only when the value is not null.

val name: String? = getName()

val length: Int? = name?.length

If name is null, length becomes null instead of throwing a NullPointerException.

3. Use the Elvis operator ?: for defaults

Use ?: to provide a fallback value when something is null.

val name: String? = getName()

val length: Int = name?.length ?: 0

You can also return early or throw an exception:

fun printName(name: String?) {
    val nonNullName = name ?: return
    println(nonNullName)
}
fun requireName(name: String?) {
    val nonNullName = name ?: error("Name is required")
    println(nonNullName)
}

4. Use if null checks and smart casts

After checking that a nullable value is not null, Kotlin smart-casts it to a non-null type.

fun printLength(text: String?) {
    if (text != null) {
        println(text.length)
    } else {
        println("Text is null")
    }
}

Inside the if block, text is treated as a non-null String.

5. Use let to run code only when non-null

?.let { ... } is useful when you want to execute a block only if the value exists.

val email: String? = getEmail()

email?.let {
    println("Sending email to $it")
}

You can combine it with Elvis for the null case:

email?.let {
    println("Sending email to $it")
} ?: println("No email available")

6. Avoid !! unless you truly mean “crash if null”

The not-null assertion operator !! converts a nullable value to a non-null value, but throws a NullPointerException if the value is null.

val name: String? = getName()
val length = name!!.length

Prefer safer alternatives:

val length = name?.length ?: 0

Use !! only when null would indicate a serious programmer error, and you want the program to fail immediately.

7. Prefer non-nullable function parameters when possible

If a function requires a value, declare it as non-nullable.

fun greet(name: String) {
    println("Hello, $name")
}

If null is a valid input, declare it explicitly:

fun greet(name: String?) {
    println("Hello, ${name ?: "Guest"}")
}

Quick guide

Situation Use
Access nullable value safely value?.property
Provide default if null value ?: default
Run code only if non-null value?.let { ... }
Check manually if (value != null)
Require non-null and crash if null value!!
Disallow null entirely Use non-null type, e.g. String

In general: prefer non-nullable types, use ?. and ?: for safe handling, and avoid !! unless absolutely necessary.

How do I combine Kotlin collections with coroutines and flows for asynchronous processing?

You typically combine Kotlin collections, coroutines, and Flow by using:

  • collections for in-memory data
  • coroutines for concurrency / async work
  • Flow for asynchronous streams of values

Basic idea

If you have a collection:

val ids = listOf(1, 2, 3, 4, 5)

You can turn it into a Flow:

val idFlow = ids.asFlow()

Then process each item asynchronously using Flow operators:

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val ids = listOf(1, 2, 3, 4, 5)

    ids.asFlow()
        .map { id ->
            fetchUser(id)
        }
        .collect { user ->
            println(user)
        }
}

suspend fun fetchUser(id: Int): String {
    delay(500)
    return "User $id"
}

Here:

  • asFlow() converts the collection into a Flow
  • map { } applies a suspending transformation
  • collect { } starts the flow and consumes results

Sequential asynchronous processing

By default, Flow processes elements sequentially:

ids.asFlow()
    .map { id ->
        fetchUser(id)
    }
    .collect { user ->
        println(user)
    }

Even though fetchUser is suspending, each item is processed one after another.

Concurrent processing with flatMapMerge

If you want to process multiple items concurrently, use flatMapMerge:

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val ids = listOf(1, 2, 3, 4, 5)

    ids.asFlow()
        .flatMapMerge(concurrency = 3) { id ->
            flow {
                emit(fetchUser(id))
            }
        }
        .collect { user ->
            println(user)
        }
}

suspend fun fetchUser(id: Int): String {
    delay(500)
    return "User $id"
}

This allows up to 3 items to be processed at the same time.

Note: flatMapMerge may emit results out of the original order.

Keeping order while doing concurrent work

If you need concurrency but want results in the original order, you can use async with a collection:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val ids = listOf(1, 2, 3, 4, 5)

    val users = ids.map { id ->
        async {
            fetchUser(id)
        }
    }.awaitAll()

    println(users)
}

suspend fun fetchUser(id: Int): String {
    delay(500)
    return "User $id"
}

awaitAll() returns results in the same order as the original list.

Filtering and transforming Flow values

You can use familiar collection-like operators:

ids.asFlow()
    .filter { id ->
        id % 2 == 0
    }
    .map { id ->
        fetchUser(id)
    }
    .collect { user ->
        println(user)
    }

This is similar to collection processing, but it supports suspending operations.

Collecting a Flow back into a collection

If you need a List again:

val users: List<String> = ids.asFlow()
    .map { id -> fetchUser(id) }
    .toList()

Because toList() collects the flow, it must be called from a coroutine or suspend function.

Using flowOn for background work

You can move upstream processing to a dispatcher:

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val ids = listOf(1, 2, 3, 4, 5)

    ids.asFlow()
        .map { id ->
            fetchUser(id)
        }
        .flowOn(Dispatchers.IO)
        .collect { user ->
            println(user)
        }
}

This is useful for I/O-bound work such as network or database calls.

Handling errors

Use catch to handle exceptions from upstream operators:

ids.asFlow()
    .map { id ->
        fetchUser(id)
    }
    .catch { error ->
        emit("Fallback user because of: ${error.message}")
    }
    .collect { user ->
        println(user)
    }

Example: process URLs asynchronously

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val urls = listOf(
        "https://example.com/a",
        "https://example.com/b",
        "https://example.com/c"
    )

    val results = urls.asFlow()
        .flatMapMerge(concurrency = 2) { url ->
            flow {
                val content = download(url)
                emit(url to content.length)
            }
        }
        .toList()

    println(results)
}

suspend fun download(url: String): String {
    delay(1_000)
    return "Content from $url"
}

When to use what

Need Use
Simple in-memory transformation Collection operators: map, filter
Suspending work per item, sequential asFlow().map { suspendCall() }
Suspending work per item, concurrent flatMapMerge
Concurrent work while preserving order map { async { ... } }.awaitAll()
Continuous stream of values Flow
Convert Flow back to List toList()

In short:

val results = items.asFlow()
    .filter { shouldProcess(it) }
    .flatMapMerge(concurrency = 4) { item ->
        flow {
            emit(processAsync(item))
        }
    }
    .toList()

That pattern is a good starting point for asynchronous collection processing with Kotlin coroutines and flows.

How do I use inline functions and reified types in collection processing?

In Kotlin, inline functions and reified type parameters are especially useful in collection processing when you want to write generic, type-safe utilities that still need access to the actual runtime type.

Normally, generic type information is erased at runtime, but reified type parameters in inline functions let you do checks like is T, as T, or filterIsInstance<T>().

Basic idea

inline fun <reified T> Iterable<*>.onlyOfType(): List<T> {
    return this.filterIsInstance<T>()
}

Usage:

val items: List<Any> = listOf("Kotlin", 42, "Java", 3.14)

val strings = items.onlyOfType<String>()

println(strings) // [Kotlin, Java]

Because T is reified, Kotlin knows at runtime that T is String.

Why inline is required

A reified type parameter is only allowed on an inline function:

inline fun <reified T> someFunction(value: Any): Boolean {
    return value is T
}

This works because the compiler substitutes the function body at the call site and preserves the concrete type.

This would not compile:

fun <T> someFunction(value: Any): Boolean {
    return value is T // Error: cannot check for instance of erased type
}

Filtering a collection by type

inline fun <reified T> List<*>.filterByType(): List<T> {
    return filterIsInstance<T>()
}

Example:

val mixed = listOf(1, "two", 3L, "four", 5.0)

val strings = mixed.filterByType<String>()
val ints = mixed.filterByType<Int>()

println(strings) // [two, four]
println(ints)    // [1]

Mapping only matching elements

You can combine reified type checks with mapNotNull:

inline fun <reified T, R> Iterable<*>.mapIfType(
    transform: (T) -> R
): List<R> {
    return mapNotNull { item ->
        if (item is T) transform(item) else null
    }
}

Usage:

val values: List<Any> = listOf("one", 2, "three", 4)

val lengths = values.mapIfType<String> { it.length }

println(lengths) // [3, 5]

Finding the first item of a type

inline fun <reified T> Iterable<*>.firstOfTypeOrNull(): T? {
    return firstOrNull { it is T } as? T
}

Usage:

val items: List<Any> = listOf(10, "hello", 20)

val firstString = items.firstOfTypeOrNull<String>()

println(firstString) // hello

A slightly cleaner version uses filterIsInstance:

inline fun <reified T> Iterable<*>.firstOfTypeOrNull(): T? {
    return filterIsInstance<T>().firstOrNull()
}

Grouping elements by runtime type

inline fun <reified T> Iterable<*>.partitionByType(): Pair<List<T>, List<Any?>> {
    val matching = mutableListOf<T>()
    val others = mutableListOf<Any?>()

    for (item in this) {
        if (item is T) {
            matching += item
        } else {
            others += item
        }
    }

    return matching to others
}

Usage:

val data = listOf("a", 1, "b", 2.0, null)

val result = data.partitionByType<String>()

println(result.first)  // [a, b]
println(result.second) // [1, 2.0, null]

Processing collections with inline lambdas

inline is also useful for performance when you pass lambdas to collection-like helper functions. It avoids allocating a function object in many cases.

inline fun <T, R> Iterable<T>.transformEach(
    transform: (T) -> R
): List<R> {
    val result = ArrayList<R>()

    for (item in this) {
        result += transform(item)
    }

    return result
}

Usage:

val numbers = listOf(1, 2, 3)

val doubled = numbers.transformEach { it * 2 }

println(doubled) // [2, 4, 6]

Combining inline and reified

A common pattern is a type-safe processing function:

inline fun <reified T, R> Iterable<*>.processType(
    transform: (T) -> R
): List<R> {
    return mapNotNull { item ->
        if (item is T) {
            transform(item)
        } else {
            null
        }
    }
}

Example:

val events: List<Any> = listOf(
    "login",
    404,
    "logout",
    500
)

val uppercasedEvents = events.processType<String> {
    it.uppercase()
}

println(uppercasedEvents) // [LOGIN, LOGOUT]

Example with sealed classes

sealed interface Event

data class ClickEvent(val x: Int, val y: Int) : Event
data class TextEvent(val text: String) : Event
data class ErrorEvent(val message: String) : Event

inline fun <reified T : Event> Iterable<Event>.ofEventType(): List<T> {
    return filterIsInstance<T>()
}

Usage:

val events: List<Event> = listOf(
    ClickEvent(10, 20),
    TextEvent("hello"),
    ErrorEvent("failed"),
    TextEvent("world")
)

val textEvents = events.ofEventType<TextEvent>()

println(textEvents.map { it.text }) // [hello, world]

Important limitations

1. reified only works in inline functions

inline fun <reified T> ok(value: Any) = value is T

But this does not work:

fun <T> notOk(value: Any) = value is T

2. It does not fully solve nested generic type erasure

This can be misleading:

val data: List<Any> = listOf(listOf("a"), listOf(1))

val stringLists = data.filterIsInstance<List<String>>()

Because of type erasure, the runtime can check that each item is a List, but not that each list contains String.

A safer approach:

val stringLists = data
    .filterIsInstance<List<*>>()
    .filter { list -> list.all { it is String } }
    .map { list -> list.map { it as String } }

Practical collection utilities

inline fun <reified T> Iterable<*>.countOfType(): Int {
    return count { it is T }
}

inline fun <reified T> Iterable<*>.containsType(): Boolean {
    return any { it is T }
}

inline fun <reified T> Iterable<*>.withoutType(): List<Any?> {
    return filterNot { it is T }
}

Usage:

val values = listOf("a", 1, "b", 2.0)

println(values.countOfType<String>())   // 2
println(values.containsType<Double>())  // true
println(values.withoutType<String>())   // [1, 2.0]

Rule of thumb

Use inline + reified when:

  • You need to check item is T
  • You need to cast with as T or as? T
  • You want to call APIs like filterIsInstance<T>()
  • You want type-safe helpers for heterogeneous collections
  • You want to avoid passing KClass<T> or Class<T> manually

For ordinary collection transformations where no runtime type check is needed, a regular generic function is usually enough:

fun <T, R> Iterable<T>.mapCustom(transform: (T) -> R): List<R> {
    return map(transform)
}