lateinit & lazy

Reviewed & published by Brayan K

lateinit and lazy are two ways to delay initialization: lateinit var lets you set a non-null property later by hand, while by lazy computes a val automatically on first access and caches it.

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.

Both avoid giving a value before you actually have one — you'll learn exactly when to reach for each.

What You'll Learn in This Lesson

1️⃣ lateinit var — Initialize Later

A lateinit var declares a non-null property with no initial value, promising you'll assign it before any read. This is common when a framework or a setup step provides the value after the object is constructed.

class Greeter {
    // 'lateinit var' promises a non-null value will be set LATER,
    // before it is read. No initializer needed up front.
    lateinit var name: String

    fun setup() {
        name = "Ada"
    }

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

fun main() {
    val g = Greeter()
    g.setup()                       // assign before use
    g.greet()                       // -> Hello, Ada!
}

Reading a lateinit property before it's set throws an exception. You can check first with ::prop.isInitialized:

class Config {
    lateinit var url: String

    fun describe(): String {
        // ::url.isInitialized guards against reading too early:
        return if (::url.isInitialized) "url = $url" else "not set yet"
    }
}

fun main() {
    val c = Config()
    println(c.describe())           // -> not set yet
    c.url = "example.com"
    println(c.describe())           // -> url = example.com
}

2️⃣ by lazy — Compute Once, On First Access

The by lazy { } delegate runs its block the first time the property is read, then caches the result. Later reads return the cached value without re-running the block. It's ideal for expensive values you might not even need.

fun main() {
    // 'by lazy { }' runs the block ONCE, on first access,
    // then caches the result for every later read.
    val heavy: String by lazy {
        println("computing...")     // prints only the first time
        "RESULT"
    }

    println("before first access")
    println(heavy)                  // triggers the block now
    println(heavy)                  // cached: no recompute
}

3️⃣ Eager val vs lazy val

A regular val is computed eagerly — right where it's declared. A by lazy val is computed lazily — not until something reads it. Watch the print order to see exactly when each block runs.

fun main() {
    // A normal val is computed RIGHT NOW (eagerly):
    val eager = 2 + 2               // computed immediately
    println("eager ready")          // before we even use it

    // A lazy val is computed only when first read:
    val deferred: Int by lazy {
        println("calculating deferred")
        10 * 10
    }
    println("deferred declared")    // block has NOT run yet
    println(deferred)               // now it runs
    println("eager = $eager")       // 4
}

Your turn. Fill in the ___ blank, then run and compare.

// 🎯 YOUR TURN — replace each ___ then run main()
fun main() {
    // 1) Declare a lazy value that computes 6 * 7 on first access
    val answer: Int by ___ {         // 👉 lazy
        42
    }

    println("starting")
    println(answer)                  // 👉 expected: 42

    // ✅ Expected:
    // starting
    // 42
}

Mini-Challenge: Lazy Database Name

Prove that lazy computes only once: the "connecting..." line must appear a single time even though you read the value twice.

// 🎯 MINI-CHALLENGE: a lazy database name.
// Save as LazyDb.kt and run main().
//
// 1. Declare 'dbName: String by lazy { ... }' whose block
//    prints "connecting..." and returns "users-db".
// 2. Print "app started" FIRST.
// 3. Then print dbName twice.
//
// Note: "connecting..." must appear only ONCE (lazy caches).
//
// ✅ Expected output:
//    app started
//    connecting...
//    users-db
//    users-db

fun main() {
    // your code here
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — lateinit & lazy

FeatureDetails
lateinit varNon-null var, set manually later
::p.isInitializedCheck before reading a lateinit
val x by lazy { }Computed once on first access
lateinit applies tovar, non-null, reference types only
lazy applies toval (any type, incl. primitives)

🎉 Lesson Complete!

Practice quiz

What does 'lateinit var' promise the compiler?

  • The value is computed automatically
  • A non-null value will be assigned before it is read
  • The property is immutable
  • The value is always null

Answer: A non-null value will be assigned before it is read. lateinit var declares a non-null property you promise to set before reading it.

What happens if you read a lateinit property before assigning it?

  • It returns null
  • It returns an empty string
  • It throws UninitializedPropertyAccessException
  • It returns zero

Answer: It throws UninitializedPropertyAccessException. Reading a lateinit property too early throws UninitializedPropertyAccessException.

How do you check whether a lateinit property has been set?

  • prop != null
  • prop.isSet()
  • ::prop.isInitialized
  • prop.exists

Answer: ::prop.isInitialized. Use the ::prop.isInitialized check to guard against reading too early.

Which of these can lateinit be applied to?

  • A val
  • An Int property
  • A var of a non-null reference type
  • A computed property

Answer: A var of a non-null reference type. lateinit works only on a var of a non-null reference type — not val or primitives.

When does a 'by lazy { }' block run?

  • The first time the property is read
  • At program startup
  • Every time it is read
  • When the object is constructed

Answer: The first time the property is read. The lazy block runs once, on first access, then caches its result.

How many times does a lazy block run if the property is read three times?

  • Three times
  • Once
  • Zero times
  • Twice

Answer: Once. lazy runs the block once and caches; later reads return the cached value.

When is a normal val computed?

  • Lazily, on first read
  • Never
  • Eagerly, right where it is declared
  • Only inside main()

Answer: Eagerly, right where it is declared. A regular val is computed eagerly, at the point of declaration.

Which delegate is appropriate for a deferred val of type Int?

  • lateinit var
  • by lazy
  • a nullable Int
  • an abstract property

Answer: by lazy. lateinit can't be used on Int or val, so use by lazy for a deferred Int val.

Why might you choose lateinit over a nullable type?

  • It allows null safely
  • It recomputes automatically
  • It avoids handling null on every access for a value you know will be set
  • It is faster to compute

Answer: It avoids handling null on every access for a value you know will be set. lateinit gives a non-null var so code skips ?. / !! checks for a value set later.

By default, is a lazy property thread-safe?

  • Yes, it is synchronized by default
  • No, never
  • Only on Android
  • Only for val of Int

Answer: Yes, it is synchronized by default. By default lazy uses synchronized mode, making first access thread-safe.

Continue this course

Frequently asked questions

What is the difference between lateinit and lazy?

lateinit is for a var you will assign manually at some later point — you take responsibility for setting it before any read, and you can change it again afterward. lazy is for a val whose value is computed automatically by a block the first time it is accessed, then cached forever. Use lateinit when something else (a framework, a setup method) provides the value; use lazy when the value can compute itself but you want to defer the cost until it is actually needed.

Why can't I just use a nullable type instead of lateinit?

You can, but then every access forces you to handle null with ?. or !!, even though you know the value will be present by the time you use it. lateinit lets you declare a non-null var without an initial value, so the rest of your code treats it as an ordinary non-null property. The trade-off is that reading it before assignment throws an UninitializedPropertyAccessException rather than giving you null.

What can lateinit be applied to, and what can't it?

lateinit works only on a var (never a val), only on non-null reference types, and not on primitive types like Int, Double, or Boolean. It also cannot have a custom getter or setter. If you need a deferred Int or a deferred val, reach for by lazy instead, which has none of those restrictions.

Does lazy recompute its block on every access?

No — that is the whole point. The block runs exactly once, on the first read, and its result is stored. Every subsequent access returns the cached value without re-running the block. By default lazy is also thread-safe (synchronized), so two threads racing to the first access still get a single, consistent computation.