JavaRush /Courses /Kotlin SELF /Initialization order: properties → init → secondary con...

Initialization order: properties → init → secondary constructor body

Kotlin SELF
Level 31, Lesson 3
Available

1. Introduction

Initialization order is like the sequence of steps in cooking: if you put a cake into the oven first and only then remember the batter is still in the bag, you get… experimental cuisine. Programming is the same: if in init you use a property that has not been initialized yet, or you assume the secondary constructor body will “manage to prepare the data,” you will get strange values, extra computations, or exceptions in unexpected places.

This lecture is here so you can confidently answer questions like: why one println() prints before another, why a check triggers before your code in the secondary constructor, why a property already has a value inside init, and where it is better to place a computation so it runs exactly once and at the right moment.

2. Mental model: what object creation consists of

When we write val e = Expense(...), it looks like a single line. But in reality, object creation is a script of steps, and Kotlin executes them in a fixed order. It is calmest to treat this like a small assembly line: first properties are prepared (initializers), then init blocks run, and only after that (if you create the object through a secondary constructor) its body executes.

The idea is: init blocks execute when an instance is created and follow the order, just like property initializers. And if a secondary constructor delegates to the primary (and when a primary exists, it must do so), then all initialization (properties + init) is guaranteed to finish before the code in the secondary constructor body.

Let’s lock in the scheme.

flowchart TD
    A["Constructor call: ClassName(...)"] --> B["Delegation to primary constructor (if needed)"]
    B --> C["Property initializers (in class order)"]
    C --> D["init blocks (in class order)"]
    D --> E{"Was it created via a secondary constructor?"}
    E -->|yes| F["Secondary constructor body { ... }"]
    E -->|no| G["Object is ready"]
    F --> G

And one more short table so it is convenient to “cross-check visually”:

Class part Where we write it When it executes
Property initializer
val x = ...
Before init, in the order of declaration in the class
init { ... }
init { ... }
After property initializers, also top to bottom in order
Secondary constructor body
constructor(...) : this(...) { ... }
After properties + init have run

One more useful landmark: assignments of properties from primary constructor parameters conceptually happen before property initializers and init blocks. That is why “the line looks like one,” but there are many steps inside.

3. Property initializers: earlier than you think

A property initializer is the expression on the right side of = when declaring a property. It looks like you “just set a value,” but semantically it is a piece of code that runs during object creation. So you should treat initializers as part of construction: they should be simple, predictable, and not turn object creation into a 12-season TV series.

The most common beginner surprise: “I thought init would run first, and then properties would be initialized.” In practice it is the other way around: property initializers run before init. That makes init a convenient place where you can safely use values of such properties (if they are declared above in the code and do not depend on something “dangerous”).

A property that depends on a constructor parameter

Let’s continue our training console mini‑project “expense tracking” (a conditional ExpenseTracker). Today we will build a piece of the model so that it demonstrates the initialization order.

class Expense(rawTitle: String, val amountCents: Int) {
    val title: String = rawTitle.trim()
    val titleLength: Int = title.length

    init {
        println("init: title='$title', length=$titleLength")
    }
}

fun main() {
    Expense("  Coffee  ", 350)
    // init: title='Coffee', length=6
}

The point here is that titleLength depends on title, and title depends on the rawTitle parameter. Kotlin allows this just fine: primary constructor parameters are available both in property initializers and in init.

When creating the object, title is computed first, then titleLength, and then init runs. That is, the order goes top to bottom through the class body: as written — so executed.

Hidden work in a property initializer

The trouble starts when a property initializer does not just compute a number/string, but does “something with side effects”: prints, touches files, hits the network (yes, some people manage that too), or depends on something that is not ready yet.

Let’s intentionally make initialization “noisy” to see the order:

class DebugExpense(val rawTitle: String) {

    val title: String = normalizeTitle()

    init {
        println("init: title='$title'")
    }

    private fun normalizeTitle(): String {
        println("property init: normalizeTitle()")
        return rawTitle.trim()
    }
}

fun main() {
    DebugExpense("  Taxi  ")
    // property init: normalizeTitle()
    // init: title='Taxi'
}

Notice: first the print from normalizeTitle() (this is the property initializer for title), then the print from init. This is exactly the “assembly line mechanics.”

If you are used to thinking “init is the main initialization place,” then yes: it is important. But property initializers are no less real a part of the process; they just often look innocent.

4. init blocks: assemble and validate

An init block is code that runs when an object is created, as part of initialization. It is usually used for two things: normalizing/preparing the state and validating the correctness of the input.

In Kotlin you can have multiple init blocks, and they execute in the order they appear in the class body, together with property initializers. This sounds simple, but in real code it often raises questions: “if there are multiple init blocks, which one runs first?” or “if I declared a property below, can I use it above?”. Let’s break it down with clear traces from println().

One init and a trace print

Let’s make a minimal “order tracker”:

class BuildOrder(val x: Int) {

    val y: Int = x + 1

    init {
        println("init: x=$x, y=$y")
    }
}

fun main() {
    BuildOrder(10)
    // init: x=10, y=11
}

Here you can see that y is already computed by the time we enter init. This is expected: property initializers come first.

Multiple init blocks: top-to-bottom order

Now let’s make two init blocks and add a property between them. This is a good technique to “feel” that the order is textual: top to bottom in the file.

class ManySteps(val name: String) {

    init {
        println("init #1: name='$name'")
    }

    val upper: String = name.uppercase()

    init {
        println("init #2: upper='$upper'")
    }
}

fun main() {
    ManySteps("Ann")
    // init #1: name='Ann'
    // init #2: upper='ANN'
}

The practical takeaway is simple: if the second init relies on some property, make sure that property is declared above it. Otherwise you either will not be able to reference it, or you will end up with a structure that is hard to read and maintain.

5. Secondary constructor: why its body is always last

A secondary constructor is often perceived as “an alternative way to create an object,” and that is true. But the key nuance is when exactly its body runs.

Beginners sometimes expect that “inside the secondary constructor body we can prepare data, and then init will run.” Kotlin’s contract is different: when a primary constructor exists, the secondary constructor must first delegate to it (this(...)), and only after property initializers and init blocks have executed does the secondary constructor body run.

This is done so the class has a single initialization path: an object should not exist “half-ready.”

Delegation to primary: a live example

Let’s take Expense and add a secondary constructor that accepts the amount as a string (as if we are reading user input).

class Expense(val title: String, val amountCents: Int) {

    init {
        println("init: title='$title', amount=$amountCents")
    }

    constructor(rawTitle: String, amountText: String) : this(
        title = rawTitle.trim(),
        amountCents = amountText.toIntOrNull() ?: 0
    ) {
        println("secondary body: amountText='$amountText'")
    }
}

fun main() {
    Expense("  Tea  ", "120")
    // init: title='Tea', amount=120
    // secondary body: amountText='120'
}

The message from init prints before the message from the secondary constructor body. This is not an accident: delegation guarantees that the core initialization runs before the secondary constructor logic.

And that is logical: init is part of the “object correctness core.” If the secondary could run earlier, you could temporarily keep the object in an invalid state.

A class without a primary: init is still before the secondary body

Sometimes there is no primary constructor at all, but there is a secondary. It is also easy to get expectations wrong here: it seems that if “there is no primary,” then init should be “after the constructor.” But no: the init block still runs before the secondary constructor body.

class NoPrimary {
    init {
        println("init runs first")
    }

    constructor(x: Int) {
        println("secondary body: x=$x")
    }
}

fun main() {
    NoPrimary(5)
    // init runs first
    // secondary body: x=5
}

If you remember this, you will stop “hoping for luck” and start designing initialization consciously: everything required for correctness goes into the shared path, and the secondary constructor remains an input adapter and, at most, a place for secondary actions (for example, logging), but not for forming the base state.

6. Practical surprises in an expense mini‑app

Now let’s tie the mechanics to real meaning. In our console ExpenseTracker, we want Expense to always be valid: the title is not empty, the amount is not negative, and we conveniently store a “short name” for display in a list.

If you distribute computations incorrectly, you can get duplication, different values depending on the constructor, or simply an unreadable mess. Next are examples that look a bit “tutorial-ish,” but that is intentional: they show not business logic, but the event order.

Mistake: hoping the secondary constructor will prepare data for init

Let’s try to make init validate title, and the secondary constructor would like to “fix” the title later. It will not work (and that is good).

class BadExpense(val title: String) {

    init {
        require(title.isNotBlank()) { "title must not be blank" }
        println("init: ok")
    }

    constructor(rawTitle: String, allowBlank: Boolean) : this(rawTitle) {
        // Too late! init has already run.
        println("secondary body: allowBlank=$allowBlank")
    }
}

If you create BadExpense(" ", true), then require(...) will fail before the secondary constructor body even starts executing. This is not a bug; it is a guarantee: init is part of the shared path.

The correct thought here is: if you need to “fix” input, do it at delegation time : this(...) (that is, before entering init) or in a property initializer / init on the shared path.

The correct pattern: prepare input before init

Let’s make the “normal” version: the secondary constructor brings input to a canonical form and passes it to the primary.

class Expense(val title: String) {

    init {
        require(title.isNotBlank()) { "title must not be blank" }
        println("init: title='$title'")
    }

    constructor(rawTitle: String, trim: Boolean) : this(
        title = if (trim) rawTitle.trim() else rawTitle
    )
}

fun main() {
    Expense("  Taxi  ", trim = true)
    // init: title='Taxi'
}

Now it does not matter which path creates the object: init always sees an already prepared title. This matches the idea “secondary is an adapter, primary+init is a single path.”

Order as an API design tool

When you understand the order, it becomes easier to decide where to put logic.

If a computation must be done once and always in the same way, it is convenient to keep it either in the delegation arguments (: this(...)), or in a property initializer, or in init. All three places execute before the secondary constructor body and form the “creation foundation.”

If an action does not affect object correctness (for example, print a log “created an expense from a string”), it is a candidate for the secondary constructor body. This way you do not clutter the main path and do not make object creation “too noisy.”

How to debug initialization

When initialization order gets confusing, the brain starts filling in “as if” — and you end up chasing phantom causes. So the best technique is to make the order visible.

The simplest tool is temporary println() in property initializers, in init, and in the secondary constructor body. Try to label messages so they differ; otherwise later you will read the log like a cipher.

A good habit is to build a minimal example: one class, one property, one init, one secondary constructor, and a couple of prints. As soon as you see the order on a simple example, transfer the understanding back into the “production” class. Kotlin is predictable here: initializers and init follow the declaration order, and the secondary body comes after them.

7. Common mistakes in understanding initialization order

Mistake No. 1: expecting init to run before property initializers.
It often feels logical: “I wrote init, so everything starts there.” But val x = ... is also initialization code, and it executes before init. Because of this, people are surprised why some computations/prints happen “before the constructor.” The right habit is to perceive property initializers as the first phase of object creation, which comes earlier than init.

Mistake No. 2: thinking the secondary constructor body runs before init and can prepare data.
This is especially popular when the secondary constructor takes a string and wants to “fix” fields, while init already validates correctness. But Kotlin guarantees the opposite: the secondary delegates to the primary, properties and init run, and only then the secondary constructor body executes. If you need to prepare data for init, do it in delegation : this(...).

Mistake No. 3: hiding heavy work in a property initializer and then being surprised by performance and logs.
A property initializer looks like an assignment, so sometimes people put computations with side effects there: printing, complex string parsing, generating something large. Then the object “becomes too expensive to create,” and the logs contain a bunch of messages “by themselves.” It is better to keep property initializers simple: compute a value from already available data without surprises, and place more noticeable logic explicitly in init (or before passing to the primary).

Mistake No. 4: confusing textual order in code with the logical order in your head.
If the second init uses a property declared below, the compiler will not let you do that, or you will start rearranging pieces at random. Discipline helps: declare properties in the order they are needed, and place init blocks near the properties whose meaning they guarantee. Kotlin executes init blocks top to bottom, and this is not an “implementation detail,” but part of readability.

Mistake No. 5: duplicating initialization and validations across multiple constructors.
When a class has multiple secondary constructors, a beginner sometimes copies the same require(...) or the same trim() into each constructor body. The result: one constructor is forgotten during an update — and the class behavior starts depending on the creation path. It is much more reliable to keep correctness rules in the shared path (in init and/or in delegation to the primary), so all creation paths lead to the same logic.

Mistake No. 6: turning object creation into a show full of side effects.
Sometimes you want to print, compute, mutate external variables, and start complex scenarios during creation. But it is better to keep object creation as clean as possible: by the end of init the object should be ready for use, not “almost ready, just three more steps left.” If you need many external actions, it is most often a sign that you mixed “model creation” and “application scenario” in one place.

1
Task
Kotlin SELF, level 31, lesson 3
Locked
Normalization trace
Normalization trace
1
Task
Kotlin SELF, level 31, lesson 3
Locked
Build Steps
Build Steps
1
Task
Kotlin SELF, level 31, lesson 3
Locked
Receipt from a form
Receipt from a form
1
Task
Kotlin SELF, level 31, lesson 3
Locked
Log without primary
Log without primary
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION