Introduction: Why Even Smart Developers Stumble on Kotlin
\n\nKotlin is an elegant and powerful language that has won the hearts of millions of developers. But its conciseness and flexibility hide pitfalls, especially for beginners coming from Java or other languages. Many believe that since Kotlin is "more understandable" than Java, there will be fewer mistakes. In practice, the opposite is true: a misunderstanding of the language's philosophy leads to bugs that are hard to catch.
\n\nIn this article, we will break down the top mistakes of beginner Kotlin developers — from simply ignoring null safety to incorrect use of coroutines. Each mistake will be illustrated with a code example and a practical tip on how to avoid it.
1. Null Safety: When "?" Saves and When It Doesn't
\n\n1.1. Ignoring Nullable Types
\nThe most common mistake is using !! (not-null assertion) where safe calls can be used. Beginners often write:
fun printLength(str: String?) {\n println(str!!.length) // Will throw NPE if str == null\n}\n\nThe correct approach is to use ?.let or the Elvis operator ?::
fun printLength(str: String?) {\n str?.let { println(it.length) } ?: println("String is null")\n}\n\n1.2. Forgetting About Smart Cast
\nKotlin automatically casts types after a null check, but beginners don't notice this:
\n\nfun process(value: Any?) {\n if (value is String) {\n // value is already String, no need to cast!\n println(value.length) // Works without (value as String).length\n }\n}\n\nTip: Trust the compiler. Use !! only if you are 100% sure the value is not null and are prepared for a crash if you are wrong.
2. Sealed Class and When: Pitfalls of Exhaustiveness
\n\n2.1. Using an else Branch with Sealed Class
\nSealed classes are designed so the compiler checks whether all variants are handled. But beginners often add else, killing all the magic:
sealed class Result {\n data class Success(val data: String) : Result()\n data class Error(val message: String) : Result()\n}\n\nfun handleResult(result: Result) {\n when (result) {\n is Result.Success -> println(result.data)\n is Result.Error -> println(result.message)\n else -> {} // Mistake! If a new subclass is added, the compiler won't warn you\n }\n}\n\nThe correct way is not to use else:
\n\nfun handleResult(result: Result) {\n when (result) {\n is Result.Success -> println(result.data)\n is Result.Error -> println(result.message)\n } // The compiler will give an error if not all branches are covered\n}\n\n2.2. Confusion Between Sealed Class and Enum
\nBeginners often choose enum where sealed class is needed. Remember: enum is for a finite set of constants, sealed class is for a hierarchy with different data in each subclass.
3. Scope Functions: apply, let, run, with, also — How Not to Get Confused
\n\n3.1. Mixing Context: this vs it
\nThe most common confusion is when a beginner uses apply (context — this) for operations where let (context — it) is needed:
data class User(var name: String, var age: Int)\n\nfun initUser(): User {\n return User("Anonymous", 0).apply {\n // this — is User\n name = "Ivan"\n age = 25\n }\n}\n\nfun printUserInfo(user: User?) {\n user?.let { \n // it — is user\n println("Name: ${it.name}, Age: ${it.age}")\n }\n}\n\n3.2. Meaningless Chains of Scope Functions
\nBeginners love to write chains like:
\n\nlist?.let { it.filter { it > 5 }.also { println(it) } }