Scope Functions (let, run, apply, also, with)

Reviewed & published by Brayan K

Kotlin is a modern, concise language whose five scope functions — let, run, apply, also, and with — let you operate on an object inside a temporary scope for cleaner, more expressive code.

Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

By the end of this lesson you'll know which scope function to reach for, based on how it references the object and what it returns.

What You'll Learn in This Lesson

1️⃣ let and run

let exposes the object as it and returns the lambda's result — perfect for null-safe transforms. run exposes it as this and also returns the result — handy when you access members directly and want a computed value back.

data class Person(var name: String, var age: Int)

fun main() {
    // let: refers to the object as 'it', RETURNS the lambda result.
    // Great for null-safety and transforming a value.
    val nameLength = "Kotlin".let { it.length }
    println(nameLength)                 // -> 6

    val maybe: String? = "hi"
    maybe?.let { println("Got: $it") }  // runs only if not null -> Got: hi

    // run: refers to the object as 'this', RETURNS the lambda result.
    val person = Person("Ada", 36)
    val summary = person.run {
        age += 1                        // 'this' is implicit
        "$name is now $age"
    }
    println(summary)                    // -> Ada is now 37
}

The maybe?.let { } pattern runs the block only when the value is present — a clean replacement for a null check.

2️⃣ apply, also, and with

apply (this receiver) returns the object — ideal for configuration. also (it receiver) also returns the object — ideal for side effects like logging. with takes the object as an argument and returns the lambda result.

data class Server(var host: String = "", var port: Int = 0)

fun main() {
    // apply: 'this' receiver, RETURNS the object itself. Ideal for setup.
    val server = Server().apply {
        host = "localhost"
        port = 8080
    }
    println(server)                     // -> Server(host=localhost, port=8080)

    // also: 'it' receiver, RETURNS the object. Good for side effects.
    val nums = mutableListOf(1, 2)
        .also { println("Before: $it") }
        .apply { add(3) }
    println(nums)                       // -> [1, 2, 3]

    // with: not an extension; takes the object as an argument,
    // 'this' receiver, RETURNS the lambda result.
    val description = with(server) {
        "Serving $host on port $port"
    }
    println(description)                // -> Serving localhost on port 8080
}

Because apply and also return the object, they chain beautifully — set up, peek, and keep going in one fluent expression.

Your turn. Replace the TODO, then run and compare.

// 🎯 YOUR TURN — replace each TODO then run main()
data class Config(var name: String = "", var retries: Int = 0)

fun main() {
    // 1) Use apply to configure the object and return it.
    val config = Config().TODO {        // 👉 replace TODO with 'apply'
        name = "prod"
        retries = 3
    }
    println(config)                     // expect Config(name=prod, retries=3)

    // 2) Use let to transform a nullable value safely.
    val input: String? = "abc"
    val len = input?.let { it.length } ?: 0
    println("Length: $len")             // expect Length: 3
}

Mini-Challenge: Build and Describe a User

Use apply to construct, with to describe, and let for a nullable.

// 🎯 MINI-CHALLENGE: build and describe a user.
// Save as Scope.kt and run main().
//
// data class User(var name: String = "", var email: String = "")
//
// 1. Use apply to create a User with name = "Sam" and
//    email = "sam@x.com".
// 2. Use with(user) to build and print a one-line description:
//    "Sam <sam@x.com>".
// 3. Use a nullable val nickname: String? = null and print
//    nickname?.let { "Nick: " + it } ?: "No nickname".
//
// ✅ Example output:
//    Sam <sam@x.com>
//    No nickname

data class User(var name: String = "", var email: String = "")

fun main() {
    // your code here
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Scope Functions

FunctionReceiverReturns
letitlambda result
runthislambda result
withthis (arg)lambda result
applythisthe object
alsoitthe object

🎉 Lesson Complete!

Practice quiz

How many scope functions does Kotlin's standard library provide?

  • Three
  • Five: let, run, with, apply, also
  • Two: let and run
  • Seven

Answer: Five: let, run, with, apply, also. The five scope functions are let, run, with, apply, and also.

Which two axes distinguish the scope functions?

  • Speed and memory
  • How the object is referenced (it/this) and what is returned
  • Nullability and mutability
  • Whether they are inline or not

Answer: How the object is referenced (it/this) and what is returned. They differ by receiver reference (it vs this) and return value (lambda result vs object).

What does apply return?

  • The lambda result
  • Nothing (Unit)
  • The object itself
  • A nullable copy

Answer: The object itself. apply returns the receiver object, making it ideal for configuration.

Which scope function uses it as the receiver and returns the lambda result?

  • let
  • apply
  • run
  • with

Answer: let. let exposes the object as it and returns the lambda's result.

What does run expose the object as, and what does it return?

  • it; the object
  • this; the lambda result
  • it; the lambda result
  • this; the object

Answer: this; the lambda result. run uses this as the receiver and returns the lambda result.

Which is the key difference between apply and also?

  • apply returns the object, also returns the result
  • apply uses this, also uses it (both return the object)
  • Only also can be chained
  • apply is for nullables only

Answer: apply uses this, also uses it (both return the object). Both return the object; apply uses this (config), also uses it (side effects).

What is the cleanest way to run a block only when a nullable value is non-null?

  • with(value) { }
  • value.run { }
  • value?.let { }
  • value!!.apply { }

Answer: value?.let { }. value?.let { } combines a safe call with let so the block runs only for a present value.

How is with different from the other four scope functions?

  • It is not an extension; it takes the object as an argument
  • It returns the object
  • It uses it as the receiver
  • It cannot return a value

Answer: It is not an extension; it takes the object as an argument. with is a regular function taking the object as an argument; it uses this and returns the lambda result.

Which scope function is best for object configuration that returns the configured object?

  • let
  • with
  • apply
  • also

Answer: apply. apply uses this and returns the object, perfect for setting several properties.

Which functions expose the object as it (rather than this)?

  • run and with
  • let and also
  • apply and run
  • with and apply

Answer: let and also. let and also use it; run, with, and apply use this.

Continue this course

Frequently asked questions

What are scope functions and why are there five of them?

Scope functions — let, run, with, apply, and also — execute a block of code on an object and give you a temporary scope to work in. They exist to make code that operates on a single object more concise and readable. There are five because they differ along two axes: how you reference the object inside the block (as it or as this), and what the call returns (the lambda's result or the object itself). Picking the right one expresses your intent clearly.

When do I use 'it' versus 'this' inside the block?

let and also expose the object as it (a normal parameter you can rename), which reads well when you treat the object as an argument or when nesting scopes. run, with, and apply expose it as this (the implicit receiver), so you can access its members directly without a prefix — ideal for configuration where you set many properties. As a rule: use it (let/also) when you mainly call methods on the value, and this (run/with/apply) when you mostly access its members.

What's the difference between apply and also?

Both return the original object (not the lambda result), which makes them perfect for chaining. The difference is the receiver: apply uses this, so it's the go-to for object configuration — Server().apply { host = ...; port = ... }. also uses it, so it's the go-to for side effects that take the object as input, like logging or validation in the middle of a chain — list.also { println(it) }. If you're setting properties, use apply; if you're doing something with the object, use also.

How do let and run help with null safety?

Combining a safe call with let or run runs the block only when the value is non-null: value?.let { ... } executes the lambda just for a present value, with it (or this for run) smart-cast to the non-null type. This is a clean alternative to an if (value != null) check, especially when you also want to transform the value and capture the result. It keeps null-handling localized and expression-based.