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
- Declaring a non-null lateinit var without an initializer
- Guarding reads with ::prop.isInitialized
- Deferring a val's computation with by lazy { }
- How lazy runs once and caches the result
- Eager val versus lazy val timing
- Choosing lateinit versus lazy for the job
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)
- ❌ "lateinit property name has not been initialized" — you read it too early. ✅ Assign it first, or guard with ::name.isInitialized.
- ❌ Using lateinit on an Int. ✅ Primitives aren't allowed; use by lazy or give a default.
- ❌ Using lateinit val. ✅ lateinit requires a var; a deferred val uses by lazy.
- ❌ Expecting a lazy block to rerun. ✅ It runs once and caches; the value never recomputes.
Pro Tips
- 💡 lazy for expensive setup: defer heavy work until something actually needs it.
- 💡 lateinit for injected values: ideal when a framework sets the property after construction.
- 💡 val + lazy ≈ a cached computed property — clean and thread-safe by default.
📋 Quick Reference — lateinit & lazy
| Feature | Details |
|---|---|
| lateinit var | Non-null var, set manually later |
| ::p.isInitialized | Check before reading a lateinit |
| val x by lazy { } | Computed once on first access |
| lateinit applies to | var, non-null, reference types only |
| lazy applies to | val (any type, incl. primitives) |
🎉 Lesson Complete!
- ✅ lateinit var holds a non-null value you assign later
- ✅ Guard early reads with ::prop.isInitialized
- ✅ by lazy { } computes a val once, on first access, then caches
- ✅ A normal val is eager; a lazy val defers its work
- ✅ Use lateinit for injected vars, lazy for deferred vals
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
- Previous: Destructuring Declarations
- Next: Checkpoint: Kotlin Basics — Combine ranges, strings, arrays and destructuring in a build — then a quiz
- Quick reference: Kotlin cheat sheet
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.