8 min read

Kotlin Language: Variables & Types Deep Dive (Part 2/2)

A detailed learning-in-public deep dive on Variables & Types in Kotlin Language, covering concept batch 2/2.

#Learning-log#Kotlin

Table of contents

  1. Context and Scope
  2. Conceptual Model & Analogy
  3. Deep Dive
  4. Any — the root supertype (non-null)
  5. Unit — “no meaningful value”
  6. Nothing — “this never returns”
  7. Implementation Patterns
  8. Common Pitfalls and Tradeoffs
  9. Technical Note
  10. Sources & Further Reading
  11. Check Your Work
  12. Hands-on Exercise
  13. Brain Teaser

Context and Scope

This deep dive covers three Kotlin fundamental types at the top and bottom of the type lattice:

  • Any — the root supertype of all non-nullable types (and the common ancestor of all classes). Think of it as Kotlin’s Java Object counterpart. For “absolutely anything including null”, use Any?. (kotlinlang.org)
  • Unit — the return type of functions that don’t return a meaningful value (Kotlin’s equivalent of Java’s void). (kotlinlang.org)
  • Nothing — the type with no values. It marks expressions and functions that never complete normally (they throw, break, return, or loop forever), enabling precise compiler reasoning about unreachable code. (kotlinlang.org)

We’ll also include migration tips for Java interop and note relevant 2023–2025 documentation updates you should be aware of.

Conceptual Model & Analogy

Picture Kotlin’s types as a funnel:

  • At the very top is Any? (everything, including null).
  • Just below is Any (everything except null).
  • At the absolute bottom is Nothing (no values at all).

Analogy: electrical adapters.

  • Any? is a universal socket that accepts any plug, including a “no plug” signal (null).
  • Any is the same socket but disallows the “no plug.”
  • Nothing is like a dead-end connector: once you route the wire there, electricity never comes back — execution can’t continue.

Formally: the unified supertype of all Kotlin types is Any?; Any is the unified supertype of non-nullable types; Nothing is the unified subtype (bottom) of all types. (kotlinlang.org)

Deep Dive

Any — the root supertype (non-null)

  • Definition: “The root of the Kotlin class hierarchy. Every Kotlin class has Any as a superclass.” Methods common to all objects include equals, hashCode, and toString. (kotlinlang.org)
  • Any vs Any?: Any is non-null. If you truly need to accept or store null too, use Any?. Generics without an explicit upper bound are implicitly bounded by Any? (not Any). (kotlinlang.org)
  • Anonymous objects and Any: if a function returns an anonymous object without a declared supertype, its static return type becomes Any and only Any’s members are accessible at the call site. (kotlinlang.org)
  • Java interop: Java’s Object maps to platform type Any! on the Kotlin side; remember that Object’s wait/notify aren’t on Any — you must use java.lang.Object explicitly for monitor operations. (kotlinlang.org)

Unit — “no meaningful value”

  • Unit is a real type with a single value, Unit. It corresponds to Java’s void at the source level. You almost never write return Unit. (kotlinlang.org)
  • Inferred for block bodies: if a function has a block body and no useful return, Kotlin infers Unit. For function type parameters you still write () -> Unit explicitly. (kotlinlang.org)
  • Java interop: a Java method that returns void appears to return Unit when called from Kotlin. If a caller uses the return, Kotlin provides Unit at the call site. (kotlinlang.org)
  • Tooling update (2024–2025): Kotlin’s “unused return value checker” (opt-in) explicitly doesn’t report dropped values of type Unit (or Nothing / Nothing?), so enabling it won’t nag on side-effecting APIs by design. (kotlinlang.org)

Nothing — “this never returns”

  • All structural jump expressions — throw, return, break, continue — have the type Nothing, which signals the rest of that path is unreachable. (kotlinlang.org)
  • Throw expressions and functions that always throw have type Nothing; common library functions with this signature include error(…) and TODO(…). (kotlinlang.org)
  • Some APIs intentionally “don’t come back,” for example kotlin.system.exitProcess(Int): Nothing. Declaring your own APIs with Nothing documents that they never complete normally and helps exhaustiveness checks and smart casts. (kotlinlang.org)

Implementation Patterns

  • Baseline, idiomatic use
    • Any for “accept anything (non-null)”; use Any? to also accept null.
    • Unit for callbacks and side-effecting operations.
    • Nothing for fail-fast helpers and early-exit branches.
// Baseline examples (Any, Unit, Nothing)
fun logAnyNonNull(x: Any) {                // Unit inferred
    println(x)                             // side-effect only
}

fun greet(name: String?): Unit {           // explicit Unit in a function type is commonly needed
    val display = name ?: return           // `return` has type Nothing; early-exit if null
    println("Hello, $display!")
}

fun fail(message: String): Nothing =       // function never returns
    throw IllegalStateException(message)

fun nameOrFail(input: String?): String =
    input ?: error("Name required")        // `error` returns Nothing; expression type is String
  • Production-grade patterns
    • Use Any? carefully for untyped payloads (serialization, logging); narrow with is checks.
    • Prefer function types with explicit Unit for callbacks in public APIs to express “caller-supplied side effect.”
    • Model “terminal” operations with Nothing for clarity and compiler help; use exitProcess in CLIs.
// Production-grade sample: CLI entrypoint with typed early exits and terminal paths
import kotlin.system.exitProcess

sealed interface CliError { val message: String }
data class UsageError(override val message: String) : CliError
data class IoError(override val message: String) : CliError

// Terminal helper: prints and terminates the process. The Nothing return documents that we won't continue.
private fun die(err: CliError): Nothing {
    System.err.println("error: ${err.message}")
    exitProcess(2)                          // returns Nothing; control flow ends here
}

fun parseArgs(argv: Array<String>): Pair<String, Int> {
    if (argv.size < 2) die(UsageError("usage: tool <host> <port>"))
    val host = argv[0]
    val port = argv[1].toIntOrNull() ?: die(UsageError("invalid port: ${argv[1]}"))
    return host to port                     // Only reached if both values are valid
}

// Example of safely accepting “anything” from a plugin boundary,
// then narrowing with smart casts and fallbacks.
fun render(value: Any?): String = when (value) {
    null -> "(null)"                        // Any? admits null
    is Int -> "Int: $value"
    is String -> "String: $value"
    else -> "(${value::class.simpleName ?: "Unknown"}) $value"
}

References for the above behaviors: throw/return/break/continue are typed as Nothing; exitProcess returns Nothing; Unit-returning function rules and Java void mapping; Any/Any? roles. (kotlinlang.org)

Common Pitfalls and Tradeoffs

  • Confusing Any with “absolutely anything”: Any is non-null; to include null, use Any?. This also matters for generic defaults: a type parameter with no explicit bound is implicitly bounded by Any?, allowing nullable arguments. If you need a non-null constraint, write . (kotlinlang.org)
  • Overusing Any in APIs: it weakens type contracts and forces callers to downcast. Prefer sealed hierarchies or generics.
  • Java interop surprises
    • Java void methods appear as returning Unit when called from Kotlin; don’t try to treat them as producing a meaningful value. (kotlinlang.org)
    • In generic positions across the Java boundary, Java commonly uses java.lang.Void as a sentinel. Kotlin’s Unit is not the same as Java’s Void; keep Void (often Void?) for cross-language generics to avoid binary/API mismatches. (kotlinlang.org)
  • Nothing correctness: only use Nothing when an API really never returns. Infinite loops should be cancellation-friendly in coroutines; otherwise, you risk resource leaks while the type system “believes” the code path is terminal. See also Kotlin’s definition of jump expressions (typed as Nothing). (kotlinlang.org)
  • Anonymous object erosion: returning an anonymous object as Any exposes only Any’s members at call sites; define and return an interface or supertype if you need richer members. (kotlinlang.org)
  • Tooling nuance (2025+): the unused return value checker won’t flag dropped Unit/Nothing results; this is by design for side-effect-only functions. If you expect a meaningful value, make sure the function’s return type isn’t Unit. (kotlinlang.org)

Technical Note

You may encounter documentation that alternately calls Any or Any? “the root supertype.” Both are correct in context:

  • Any is the unified supertype of all non-nullable types. (kotlinlang.org)
  • Any? is the unified supertype when considering the full universe of types including null. (kotlinlang.org)

When designing APIs, decide whether null must be representable. If yes, use Any? (or a more specific nullable type); otherwise prefer a precise non-null type (often not Any at all).

Sources & Further Reading

Check Your Work

Hands-on Exercise

  1. Write a function parseUserIdOrExit(s: String?): Int that:
  • Returns an Int if s is a valid decimal.
  • Prints an error and terminates the process with a non-zero exit code otherwise.
  • Requirements: model the “terminate” helper with a Nothing return type; don’t use exceptions for control flow.
  1. Create a public API:
  • fun withLock(lock: java.util.concurrent.locks.Lock, action: () -> Unit)
  • Explain in KDoc why action returns Unit and why the API doesn’t expose Any or Any? unnecessarily.
  1. Build a small render(value: Any?) function that pretty-prints null, numbers, and everything else by class name. Demonstrate how using Any? enables passing null and how you narrow with when/is.

Brain Teaser

  • Why does the following compile, and what type is the entire expression? What does that tell you about Nothing and type inference? val username: String = getFromEnv(“USER”) ?: return Explain which part has type Nothing and which part has type String, and why the overall expression type is String. Then, switch return to throw IllegalStateException(“No USER”) and explain why both forms still type-check. Reference the relevant rules about jump expressions and Nothing. (kotlinlang.org)

Share

More to explore

Keep exploring

Previous

Weekly Engineering Mastery Quiz (2026-03-02 to 2026-03-06)

Next

Kotlin to C/C++ Transition Guide: A Systems Programmer's Cheat Sheet