What is the difference between Any, Unit, and Nothing?
Understand what Kotlin means when code produces a value, no useful result, or no result at all.
What the interviewer is testing
This looks like a type-system question, but interviewers are usually trying to understand how you think about code paths.
Can you tell the difference between returning a value, completing work without returning anything useful, and reaching a point in the code that can never successfully return?
That's the mental model they're looking for. It shows you understand what a piece of code can actually produce, not just the names of Kotlin's types.
❌ Common mistake
If they ask you to elaborate
Any means the specific type has been widened away
Every non-null Kotlin type has Any as a supertype.
That makes code like this valid:
val value: Any = User("Ada")The User still exists, but through the name value you can only rely on what Any guarantees. The more specific User API is no longer visible without narrowing or casting the type again.
The nullable distinction matters too. Any itself does not include null; Any? is the type that can represent any Kotlin value, including null.
That is why Any is useful when the type genuinely can vary, but usually a poor return type when the API actually knows something more specific.
Unit means the operation completed
A function that performs work but does not produce a useful result has the return type Unit:
fun trackScreen(name: String): Unit {
analytics.track(name)
}Kotlin normally lets you omit : Unit, but the type is still there.
The important difference from Java-style void is that Unit participates in Kotlin's type system. It has a single value, also called Unit, which makes function types such as callbacks straightforward:
fun onRetry(action: () -> Unit) {
action()
}The callback returns normally; its result just is not the point of the API.
Nothing means there is no successful continuation
Nothing has no values at all. Kotlin uses it for expressions that never complete normally.
That is why this works:
val user = repository.findUser(id)
?: throw UserNotFoundException(id)The left side of the Elvis operator produces a User when one exists. The throw expression has type Nothing, so it never needs to provide the missing User; execution stops on that branch instead.
The same idea explains why TODO() can sit inside a function that promises to return almost any type:
fun loadProfile(): Profile {
TODO("Load profile")
}TODO() returns Nothing. It does not pretend to create a Profile; it tells the type system that this path never produces a value at all.
Production thinking
The production value of these types is mostly about making APIs tell the truth.
If a function really returns a User, exposing it as Any throws away useful information and pushes type checks or casts onto the caller. Any is appropriate when the API genuinely accepts or produces unrelated types, but it should not be used as an escape hatch from modelling the data properly.
Unit is useful for command-style APIs and callbacks where the important thing is that some work happens:
fun updateProfile(
profile: Profile,
onComplete: () -> Unit,
) {
repository.update(profile)
onComplete()
}The callback communicates an event, not a computed value.
Nothing is useful when failure or termination is part of the contract. Small helpers such as error(), TODO(), or your own fail-fast functions can return Nothing, which lets the compiler understand that successful execution cannot continue from that point.
The judgement is not to force these types into code. It is to recognise what they communicate: how much type information is available, whether an operation completes normally, and whether a value can exist on that path at all.
Code example
The three ideas become clearer when you put them side by side:
fun describe(value: Any): String {
return value.toString()
}
fun logIn(user: User): Unit {
sessionStore.save(user)
}
fun missingUser(id: String): Nothing {
throw UserNotFoundException(id)
}describe() receives some non-null value without knowing its more specific type. logIn() completes normally but exists for its side effect. missingUser() has no successful return path at all.
Those are three different contracts, not three different spellings for "no type."