JavaRush/Courses/Kotlin SELF/Compiler messages about nullability

Compiler messages about nullability

Open

1. Introduction

If you are just getting started, the compiler can easily feel like a grumpy instructor who, instead of “not like that,” writes: “Type mismatch: inferred type is String? but String was expected” — and rides off into the sunset. But in reality, the Kotlin compiler is pretty honest: it almost always tells you exactly what you are trying to do and why it is dangerous. Our goal is to learn to see behind the “scary phrase” a concrete everyday situation: “I might not have a value, but I am acting as if I definitely do.”

From today on, we will look at nullability messages as hints rather than punishment. In IntelliJ IDEA (especially in K2 mode) it is even nicer: the IDE highlights the problematic spot and often suggests quick fixes.

Main idea: T? is being used as T

When you see errors related to null, it helps to keep a simple model in mind: Kotlin divides values into two “castes.” In the first caste live regular types (String, Int) — they promise: “the value exists.” In the second live nullable ones (String?, Int?) — they honestly warn: “the value might not exist.”

If you try to access members of an object (.length, .uppercase()), pass a value into a function that expects non-null, or do arithmetic, the compiler will ask: “what happens if that is null?” And until you answer that question with code, it will not compile your program.

A small “thinking flowchart”:

flowchart TD
    A[Got a value of type T?] --> B{What do we do next?}
    B --> C[Check for null and choose a branch]
    B --> D[Use ?. to access safely]
    B --> E[Use ?: to supply a default]
    B --> F[Use !! and risk a crash]
    C --> G[Inside non-null branch: work as with T]

These are not “four different philosophies,” but four different answers to one question: how exactly do we handle the absence of a value.

Translator table: message → cause → typical fix

Compiler messages are scary mostly because they look like “a letter from the IRS.” Let’s turn them into a normal human table. Here are the most common wordings you will run into at this stage.

Compiler message (typical) What it means in plain English What to look at in code What usually fixes it
Type mismatch: inferred type is String? but String was expected You are giving String? where a String is required The type on the left/right, the function parameter type ?: (default), early return, if check (x == null)
Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver You are calling a method/property on T? as if it were T The spot where the dot is: x.something x?.something, or if (x != null) x.something
Operator call corresponds to a dot-qualified call ... which is not allowed on a nullable receiver Same thing, but for operators (+, *) An operation on Int?, Double? Remove nullable: ?:, null check, do not multiply “maybe-numbers”
Smart cast to 'String' is impossible, because ... The compiler cannot guarantee the value will not change Usually a var, an object property, or “too far” from the check Snapshot into a val, move usage closer to the check, ?./?:
Unnecessary safe call on a non-null receiver You wrote ?., but the left side is already non-null The variable type is already T Remove ?. (not an error, just cleanup)

Important: today we are not “memorizing the message text.” We are learning to guess the meaning from the structure: almost always there are hints like expected/found, nullable receiver, smart cast impossible.

2. Fixing algorithm: where you must decide the fate of null

When you see an error, you want to do the fastest thing: put !! and “let it burn.” That is a human impulse, no judgment. But let’s agree: !! is like kicking a door in. Sometimes the door really is locked and you are a firefighter. But more often you just left the key in another jacket.

Instead, we will act with a calm algorithm.

First, you find where exactly nullable appeared. At this stage of the course, the most common sources are toIntOrNull() and variables you explicitly typed as String?/Int?. Then you answer the question: what do we want to do if it is null? In essence, there are only a few options: show a message, supply a default, ask for input again, or gracefully stop the scenario.

And only after that do you pick the technique: an if check, a safe call ?., Elvis ?:, or, in a rare case, !!.

To make it practical, let’s walk through this with a mini app that we will “grow” with examples.

Mini app: user form and where errors are born

Imagine a simple program: it asks for a name and an age, then prints a greeting. On paper everything looks nice. In real code, the user enters “twenty eight,” extra spaces, or just presses Enter for philosophical reasons.

Let’s start with the straightforward variant — it will almost certainly lead you to compiler messages that we are learning to translate today.

fun main() {
    print("Enter name: ")
    val name = readln().trim()

    print("Enter age: ")
    val age = readln().trim().toIntOrNull()

    println("Hi, $name! You are $age years old.")
    // Example output: Hi, Alice! You are 28 years old.
}

At first glance — fine. But the type of age here is Int?, and that starts to “infect” the rest of the code: you print “you are null years old,” and if later you want to compare the age or do math, the compiler will start protesting.

And this is exactly the key point: compiler messages appear not “because Kotlin is mean,” but because you have not made a decision yet: what to do on invalid input.

3. Error #1: Type mismatch ... String? vs String

You will hit this message very quickly when you try to return a nullable value where you promised non-null.

For example, you decided to write a function that “normalizes a name”:

fun normalizeName(raw: String?): String {
    return raw?.trim()
}

This will not compile, and you will see an error roughly like:

Type mismatch: inferred type is String? but String was expected

Why? Because raw?.trim() is a String?. If raw == null, the result is also null. But in the signature you promised to return String (i.e., “the string definitely exists”).

The fix depends on the meaning. The simplest (and often correct for UI/output) option is to provide a default via Elvis:

fun normalizeName(raw: String?): String {
    return raw?.trim() ?: "Unnamed hero"
}

fun main() {
    println(normalizeName("  Alice  ")) // Alice
    println(normalizeName(null))        // Unnamed hero
}

Notice the essence of the error: the compiler is not asking you to “add !!.” It is asking you to decide what to do if raw is missing.

The same principle works with numbers:

fun parseAge(raw: String): Int {
    return raw.trim().toIntOrNull()
}

Here you will get: Type mismatch: inferred type is Int? but Int was expected.

The solution is again about meaning. If you believe that on an invalid age you should substitute 0 (not always logical, but sometimes acceptable), then:

fun parseAge(raw: String): Int {
    return raw.trim().toIntOrNull() ?: 0
}

fun main() {
    println(parseAge("42"))     // 42
    println(parseAge("oops"))   // 0
}

But if 0 “masks the problem,” it is better to return Int? and force the calling code to decide what to do. But that is a different contract — and the compiler is exactly what forces you to choose a contract consciously.

4. Error #2: Only safe (?.) or non-null asserted (!!.) calls are allowed...

This message usually appears when you do something like:

fun main() {
    val nickname: String? = null
    println(nickname.length)
}

null has no length (it is not a String object at all), so Kotlin does not let you write code that can potentially crash.

Decoding the error: you are trying to access a member (length) on a value that may be absent. The ways to fix it again depend on the meaning.

If “no nickname — fine” is good enough, use a safe call:

fun main() {
    val nickname: String? = null
    val len: Int? = nickname?.length
    println(len) // null
}

If you need a guaranteed Int, add Elvis:

fun main() {
    val nickname: String? = null
    val len: Int = nickname?.length ?: 0
    println(len) // 0
}

If you need one behavior when a nickname exists and another when it does not, an if check is often more readable:

fun printNicknameInfo(nickname: String?) {
    if (nickname == null) {
        println("No nickname") // No nickname
        return
    }
    println("Nickname '$nickname', length = ${nickname.length}")
}

fun main() {
    printNicknameInfo(null)
    printNicknameInfo("neo")
}

What matters here is that after return the compiler understands: from here on, nickname is definitely not null. That is smart cast in a “linear style.”

5. Error #3: nullable arithmetic and operators

Sometimes you do not write a dot; you write a + b, and you still get a message about a nullable receiver. This can be surprising: “but I didn’t call a method!” However, in Kotlin many operators are syntactic sugar over function calls.

For example:

fun main() {
    val a: Int? = "10".toIntOrNull()
    val b: Int = 5

    val sum = a + b
    println(sum)
}

This will not compile because a is Int?. And plus (what is behind +) expects a normal Int on the left.

The fix is again to “remove nullable.” The most direct option is to supply a default.

fun main() {
    val a: Int = "10".toIntOrNull() ?: 0
    val b: Int = 5

    val sum = a + b
    println(sum) // 15
}

Or, if you want to stop the scenario on an error:

fun main() {
    val aOrNull = "oops".toIntOrNull()
    if (aOrNull == null) {
        println("That is not a number") // That is not a number
        return
    }

    val sum = aOrNull + 5
    println(sum)
}

Notice a characteristic pattern: val x = ... ?: return. It helps keep code flat, without extra nested ifs.

6. Error #4: Smart cast ... is impossible

This is the most annoying message, because you seem to have done everything correctly: you checked for null, and the compiler still says “I can’t.”

A classic example is a var:

fun main() {
    var text: String? = "hi"

    if (text != null) {
        println(text.length) // sometimes the compiler may start arguing right here in more complex cases
    }
}

In a simple case, the compiler will allow it. But if you complicate the picture a bit (for example, change text between the check and the usage), the trust disappears:

fun main() {
    var text: String? = "hi"

    if (text != null) {
        text = null
        println(text.length) // not allowed anymore
        println(text?.length)   // allowed
    }
}

Translation of the smart cast error: the compiler cannot guarantee the value stayed the same.

One of the simplest ways to “restore trust” is to take a snapshot into a val (an immutable reference) and work with that:

fun main() {
    var text: String? = "hi"
    val snapshot: String? = text

    if (snapshot != null) {
        println(snapshot.length) // 2
    }
}

This technique looks almost funny (“why copy a variable into a variable?”), but meaning-wise it is powerful: you freeze the value at a point in time and then work with something that cannot be reassigned.

7. Building the form like an adult

Compiler warnings: it is not “scolding,” it is helping

Errors stop the build. Warnings do not, but they often hint that your code has become cleaner.

A very typical case: you already made a variable non-null, but out of habit you keep writing ?..

fun main() {
    val name: String = "Alice"
    println(name?.length) 	 // warning: unnecessary safe call
    println(name.length)     // 5
}

The warning here is not nitpicking. It says: “you are already in a safe zone; you do not need a helmet.”

Another example is when Elvis is meaningless:

fun main() {
    val x: Int = 10
    val y = x ?: 0 // this will not compile at all: x is not nullable
    println(x) // 10
}

Here the compiler will not allow ?:, because the left side is not T?. This is also useful protection: Elvis is a tool specifically for the nullable branch.

Form: types are clear, branches are explicit

Now let’s return to our form and make it so that the compiler is happy and the behavior on errors is predictable.

Let’s adopt this rule: the name must be non-empty, the age must be a number. If the age is not a number, we print a message and exit the program.

fun main() {
    print("Enter name: ")
    val name = readln().trim()

    if (name.isEmpty()) {
        println("Name cannot be empty") // Name cannot be empty
        return
    }

    print("Enter age: ")
    val age = readln().trim().toIntOrNull() ?: run {
        println("Age must be a number") // Age must be a number
        return
    }

    println("Hi, $name! You are $age years old.") // Example: Hi, Alice! You are 28 years old.
}

Notice two things.

First, age here becomes Int, not Int?, because ?: either yields a number or ends the program. That means no arithmetic or comparisons later will be “infected” by nullable.

Second, we explicitly chose the behavior on error. And that is exactly what the compiler kept asking you to do — it just expressed it in bureaucratic language.

When !! is appropriate, and why the compiler discourages it so strongly

Since we keep saying “don’t fix everything with !!,” let’s be honest: !! does not exist for decoration. It is needed when you consciously choose the contract “if this is null here, that is a program bug.”

For example, you made some internal variable nullable only because it came from an API that way, but by the logic of your scenario it must be filled in by this point. Then !! turns into a “guard dog”: if the logic is violated, the program crashes immediately.

But in learning console scenarios with user input, !! is almost always a bad idea: the user is not obligated to follow your contract; they are just living their life.

Mini example (what it looks like and why it is risky):

fun main() {
    val ageText = readln().trim()
    val age = ageText.toIntOrNull()!!
    println("age = $age")
}

This compiles, but if the input is not a number, the program will crash. The compiler warned you, and you ignored it. At the very least, do this only when crashing really is the desired behavior.

8. Common mistakes

Mistake #1: “fixing compilation” instead of logic.
Often a beginner sees red highlighting, adds ?: 0 or !!, and is happy that it “works.” But meaning-wise the program starts lying: age becomes 0, name becomes “anonymous,” and the user gets weird results with no explanation. The right approach is to first decide what to do on null, and only then choose an operator.

Mistake #2: confusing null with an “empty value.”
null means “there is no value,” while "" means “the string exists but is empty.” If you agreed that an empty string is not allowed for a name, that is not a reason to make it String?. It is a reason to check isEmpty()/isBlank() and print a message. Nullable does not replace validation.

Mistake #3: smearing null handling across the whole codebase.
If you got a Int? from toIntOrNull() and then dragged it through five functions, you will get five places where the compiler will “remind” you about nullable. It is much easier to handle null close to where it appears: turn Int? into Int via ?: or end the scenario. Then the rest of the code becomes simpler.

Mistake #4: hoping for smart cast “forever.”
The check if (x != null) makes x safe only within a clear scope and only while the compiler is sure x will not change. If x is a var, if you move far away from the check, or if the value may change, smart cast breaks. In such cases, either ?./?: helps, or a “snapshot” into a val.

Mistake #5: using ?. as if it “supplies a default.”
?. does not supply anything. It just returns null if the left side is null. If you need a default (for example, length 0), that is the job of ?:. Confusing these roles leads to chains like val x = text?.length and unexpected null later in the program.

1
Task
Kotlin SELF,  11 level,  5 lecture
Locked
Chat name
Chat name
1
Task
Kotlin SELF,  11 level,  5 lecture
Locked
Nickname length
Nickname length
1
Task
Kotlin SELF,  11 level,  5 lecture
Locked
Entrance checkout
Entrance checkout
1
Task
Kotlin SELF,  11 level,  5 lecture
Locked
Note cache
Note cache
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet