Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Kotlin  Module 3 – Null safety

Smart casts, is checks and safe casts

Check types with is, rely on the smart casts that follow in if, when and &&, convert values safely with as?, and see when the compiler refuses a smart cast.

  • Beginner
  • 15 minutes
  • Examples run with Kotlin 2.4.20
  • By MySmartCoPilot
Free core, Pro depth: 6 of 15 minutes free
Free: An is check tells the compiler the type, and the cast is done for you · as? converts a value or gives null, and as throws. With a pass: 2 in-depth parts and 3 interview answers, about 9 minutes.

What you will learn

  • Check types with is and !is, and use the smart casts that follow in if, when and &&
  • Convert values with as? and the Elvis operator, and predict when as throws instead
  • Explain why a smart cast is refused on a var property or a custom getter, and fix it with a local val
  • Choose between require, check and error for a wrong value, and predict the exception each one throws

Before you start

What you’ll learn · Before you start

What you will learn

  • Check types with is and !is, and use the smart casts that follow in if, when and &&
  • Convert values with as? and the Elvis operator, and predict when as throws instead
  • Explain why a smart cast is refused on a var property or a custom getter, and fix it with a local val
  • Choose between require, check and error for a wrong value, and predict the exception each one throws

Before you start

On this page

A booking request arrives as JSON and is read into a Map<String, Any?>. The seats field should be an Int and the PNR a String, but every value has the type Any?, and the compiler will not even add 1 to an Any?. An is check asks what a value really is while the program runs, and after it the compiler treats the value as that type, with no cast written.

You will check types with is and !is and rely on the smart casts that follow. You will convert values with as?, which gives null where a plain cast would throw, and see where the compiler refuses a smart cast and why it is right to.

An is check tells the compiler the type, and the cast is done for you

value is Int is true when the value is an Int while the program runs, and value !is Int is its opposite. null is not an instance of any class, so a check against a non-null type such as is Int is false for it, while a nullable type such as String? includes it, as the last line of the program below shows. Beyond answering a question, is checks change what the compiler knows. In the code that runs only when a check was true, the compiler treats the value as the checked type: a smart cast: The compiler's own cast: after a check such as x != null or x is String, it treats x as the checked type wherever the check still holds, with no cast written.. Smart casts are why the branches below read value + 1 and value.length without a cast:

Classify.kt: five fields of a request, told apart by type Kotlin · classify/Classify.kt
// A booking request read from JSON: every field arrives as Any?, whatever it holds.
fun describe(value: Any?): String = when (value) {
    null -> "missing"
    is Int -> "an Int; plus one is ${value + 1}"
    is String -> "a String of ${value.length} characters"
    is List<*> -> "a list of ${value.size} items"
    else -> "a ${value::class.simpleName}, not handled"
}

fun main() {
    val request: Map<String, Any?> = mapOf(
        "seats" to 2,
        "pnr" to "8240117365",
        "legs" to listOf("NDLS-AGC", "AGC-BPL"),
        "fare" to 1250.5,
        "coupon" to null,
    )
    for ((field, value) in request) {
        println("$field: ${describe(value)}")
    }
    // null is no instance of a class, but a nullable type includes it.
    val coupon = request["coupon"]
    println("coupon is String: ${coupon is String}, coupon is String?: ${coupon is String?}")
}

Output

seats: an Int; plus one is 3
pnr: a String of 10 characters
legs: a list of 2 items
fare: a Double, not handled
coupon: missing
coupon is String: false, coupon is String?: true

Recorded with Kotlin 2.4.20 on OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: kotlinc Classify.kt -include-runtime -d main.jar && java -jar main.jar

Each branch of the when sees value as the type it checked: an Int to add to, a String with a length, a list with a size. List<*> checks only that the value is a list, because the types of its items are not kept while the program runs, so a check cannot ask for a list of strings. In the else branch value is an Any, since null was handled first.

When there is no smart cast

A smart cast needs a value that cannot change between the check and its use, so the compiler refuses one on a var property and on a property with a custom getter. Copy such a property into a local val, then check and use the copy.

The same works after an early return and on the right of &&. Step through farePaise for three of the values the program below passes it, and watch what the compiler knows after each check:

Interactive · stage by stage What the compiler knows about value, line by line Step through farePaise for the String "450", then the Int 300, then the Double 4.5. Watch the type the compiler gives value after each check.
What the compiler knows about value, line by line, step 7 of 7. Showing farePaise(value: Any?) (value = 4.5, an Any?), if (value == null) … (not null: Any from here), if (value is String) … (not a String: go on), if (value !is Int) … (not an Int: returns -1), return value * 100L (not reached). In focus: if (value !is Int) ….farePaise(value: Any?)value = 4.5, an Any?if (value == null) …not null: Any from hereif (value is String) …not a String: go onif (value !is Int) …not an Int: returns -1return value * 100Lnot reached

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The String "450". farePaise is called with the String "450". Its parameter is declared Any?, so at the start the compiler knows nothing more: value could be any type, or null.
  2. Not null. "450" is not null, so the first if does not return. On every line after it, the compiler knows that value is not null: its type there is Any, not Any?.
  3. A String. value is String is true, so inside this if the compiler smart-casts value to String. value.toLong() needs no cast, and the function returns 45000.
  4. The Int 300. Now the Int 300. It is not null, and value is String is false, so the function goes on. At this point the compiler still knows only that value is an Any.
  5. No early return. value !is Int is false for 300, so this if does not return. After it, value can only be an Int, and the compiler smart-casts it for the last line.
  6. The last line. value * 100L multiplies an Int by a Long with no cast written, and the call returns 30000, as KnownType.kt printed.
  7. The Double 4.5. 4.5 is not null and not a String, and value !is Int is true for it, so the function returns -1 and never reaches the last line.

KnownType.kt: the known type after a null check, is and !is Kotlin · known-type/KnownType.kt
// The type the compiler knows for value changes after each check.
fun farePaise(value: Any?): Long {
    if (value == null) return 0
    // From here on, value is an Any: null has been ruled out.
    if (value is String) return value.toLong() * 100
    if (value !is Int) return -1
    // From here on, value is an Int.
    return value * 100L
}

// On the right of &&, the check on its left still holds.
fun isPnr(value: Any?): Boolean = value is String && value.length == 10 && value.all { it.isDigit() }

fun main() {
    for (value in listOf(null, "450", 300, 4.5)) {
        val type = value?.let { it::class.simpleName } ?: "null"
        println("$value ($type) -> ${farePaise(value)} paise")
    }
    for (value in listOf<Any>("8240117365", 8240117365L, "82401")) {
        println("isPnr(${value::class.simpleName} $value) = ${isPnr(value)}")
    }
}

Output

null (null) -> 0 paise
450 (String) -> 45000 paise
300 (Int) -> 30000 paise
4.5 (Double) -> -1 paise
isPnr(String 8240117365) = true
isPnr(Long 8240117365) = false
isPnr(String 82401) = false

Recorded with Kotlin 2.4.20 on OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: kotlinc KnownType.kt -include-runtime -d main.jar && java -jar main.jar

After if (value !is Int) return -1, the rest of the function can only run with an Int, so value * 100L needs no cast. In isPnr, the right side of && runs only when value is String was true, so value.length compiles there.

Key idea

After a check that was true, the compiler treats the value as the checked type wherever the check still holds: in the branch, after an early return, and on the right of &&.

Checkpoint

Check: is checks and the smart casts after them

  1. Question 1 of 2 In describe of Classify.kt, value is declared Any?. What type does the compiler give value inside the branch is String -> …?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: String

    In the code that runs only after a check was true, the compiler treats the value as the checked type: a smart cast, with no cast written.

  2. Question 2 of 2 What does this print?

    Read the code, then choose one answer.

    fun seats(value: Any?): Int {
        if (value !is Int) return 0
        return value * 2
    }
    
    fun main() {
        println(seats("3"))
        println(seats(3))
    }
    Show the answer: Question 2 of 2

    Answer: 0, then 6

    A negative check followed by an early return leaves only the checked type for the lines after it, so they read the value as that type.

as? converts a value or gives null, and as throws

A cast states the type a value should have. value as Int gives the value as an Int or throws a ClassCastException, while value as? Int is a safe cast: A cast written with as?: x as? T gives x as a T when it is one, and null otherwise, instead of throwing a ClassCastException., which gives null instead of throwing. Safe casts pair naturally with the Elvis operator:

SafeCast.kt: as? with a fallback, and as when the type is wrong Kotlin · safe-cast/SafeCast.kt
// as? converts a value to a type or gives null; as converts it or throws.
fun main() {
    val request: Map<String, Any?> = mapOf("seats" to "2", "pnr" to "8240117365", "coupon" to null)

    // "2" is a String, not an Int, so as? gives null and the Elvis operator supplies 1.
    val seats = request["seats"] as? Int ?: 1
    println("seats: $seats")

    val pnr = request["pnr"] as? String
    println("pnr: $pnr, ${pnr?.length} characters")

    println("coupon: ${request["coupon"] as? String}")

    try {
        val strict = request["seats"] as Int
        println("strict seats: $strict")
    } catch (e: ClassCastException) {
        println("as Int threw ${e::class.simpleName}: ${e.message}")
    }
}

Output

seats: 1
pnr: 8240117365, 10 characters
coupon: null
as Int threw ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')

Recorded with Kotlin 2.4.20 on OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: kotlinc SafeCast.kt -include-runtime -d main.jar && java -jar main.jar

The seats field holds the text “2”, not the number 2, so as? Int gives null and the fallback, 1, is used: a cast checks the type and never parses text, which is the job of toIntOrNull(). The last line shows what as does with the same field. It throws, and its message names both classes but not the field.

Use is checks in a when when each type needs its own handling, as in describe, and safe casts with ?: when one type is expected and anything else gets a fallback. Keep as for places where another type can only be a bug. require and check stop a function with an exception when a condition fails, and the compiler smart-casts after them; error throws every time it runs, for a branch that should never run.

Checkpoint

Check: as? against as

  1. Question 1 of 2 The seats field of a request holds the text "2". What does request["seats"] as? Int ?: 1 give?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 1

    x as? T gives x as a T when it is one, and null otherwise; with ?: after it, any other type gets the fallback.

  2. Question 2 of 2 A partner sends seats either as a number, 2, or as text, "2". Which expression reads both and gives 1 for anything else?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: val v = request["seats"], then when (v) { is Int -> v; is String -> v.toIntOrNull() ?: 1; else -> 1 }

    A cast only checks the type. When a value can arrive in more than one type, an is check per type, in a when, handles each one, and text is parsed rather than cast.

When the compiler refuses a smart cast, reproduced and fixed Pro: opens with a Pro, Premium or Ultimate pass

A smart cast promises that the value checked is the value used. This part reproduces each case where the compiler cannot keep that promise, shows the failure it prevents, and fixes each one.

In this part:

  • Four checks the compiler cannot trust
  • Why the compiler is right: a getter that changes between reads
  • Three fixes that read the value once

3 runnable examples (Kotlin) · about 5 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Failing fast with as, require, check and error, measured Pro: opens with a Pro, Premium or Ultimate pass

When a value has the wrong type or breaks a rule, the program should stop where the mistake is and say what it was. This part measures what each tool throws and shows the smart casts that follow them.

In this part:

  • What as and as? do with a wrong type, and with null
  • require, check and error report three kinds of mistake
  • Smart casts after require, and contracts of your own

6 runnable examples (Kotlin) · about 9 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Practise: describe any value

This program checks a function that describes a value of any type. As given, three of its six checks fail:

Describe.kt: six checks, three of them fail Kotlin · practice/Describe.kt
// Practice: fix describe so that all six checks pass. Use a when with is checks; anything else is "unknown".
fun describe(value: Any?): String = when (value) {
    is Int -> "Int $value"
    is String -> "String of length ${value.length}"
    else -> "unknown"
}

fun main() {
    val checks = listOf(
        Triple("7", 7, "Int 7"),
        Triple("7L", 7L, "Long 7"),
        Triple("\"abc\"", "abc", "String of length 3"),
        Triple("listOf(1, 2)", listOf(1, 2), "List of size 2"),
        Triple("null", null, "null"),
        Triple("4.5", 4.5, "unknown"),
    )
    var passed = 0
    for ((shown, value, want) in checks) {
        val got = describe(value)
        val ok = got == want
        if (ok) passed++
        println("${if (ok) "ok  " else "FAIL"} describe($shown) = $got, want $want")
    }
    println("$passed of ${checks.size} checks pass")
    if (passed < checks.size) kotlin.system.exitProcess(1)
}

Output (exit status 1)

ok   describe(7) = Int 7, want Int 7
FAIL describe(7L) = unknown, want Long 7
ok   describe("abc") = String of length 3, want String of length 3
FAIL describe(listOf(1, 2)) = unknown, want List of size 2
FAIL describe(null) = unknown, want null
ok   describe(4.5) = unknown, want unknown
3 of 6 checks pass

Recorded with Kotlin 2.4.20 on OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: kotlinc Describe.kt -include-runtime -d main.jar && java -jar main.jar

Copy it, add the branches that describe is missing, and run it with kotlinc Describe.kt -include-runtime -d describe.jar && java -jar describe.jar until all six checks pass. First hint: a Long is not an Int, a list is matched by is List<*>, and null can be a branch of its own.

The questions below cover the two ideas of the first two sections, the one you know least first:

Practice that adapts to you

Practise this lesson

5 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 5 coupon holds null. What do coupon is String and coupon is String? give?

    Choose one answer.

    Show the answer: Question 1 of 5

    Answer: false, then true

    An is check against a non-null type is false for null; against a nullable type such as String? it is true.

  2. Question 2 of 5 Does if (value is String || value.length > 0) compile, where value is an Any?

    Choose one answer.

    Show the answer: Question 2 of 5

    Answer: No; on the right of ||, value is String was false, so value is not a String there

    && passes a true check to its right side, || a false one. Write value !is String || value.length == 0 to use the smart cast on the right of ||.

  3. Question 3 of 5 Which check can a when use to match a list, whatever its items?

    Choose one answer.

    Show the answer: Question 3 of 5

    Answer: is List<*>

    Generic type arguments are erased while the program runs, so a check can only test the class: is List<*>, as describe in Classify.kt does.

  4. Question 4 of 5 What does request["coupon"] as? String give when the coupon field holds null?

    Choose one answer.

    Show the answer: Question 4 of 5

    Answer: null

    x as? T gives null whenever x is not a T, null included.

  5. Question 5 of 5 What did request["seats"] as Int do in SafeCast.kt, where the field held "2"?

    Choose one answer.

    Show the answer: Question 5 of 5

    Answer: It threw a ClassCastException naming java.lang.String and java.lang.Integer

    as is for values that must have the type; when they do not, it throws, and the message says nothing about where the value came from.

Interview questions

  1. Fresher · Warm-up

    What is a smart cast in Kotlin, and when does the compiler refuse to make one?

    A smart cast is the compiler's own cast. After a check such as x != null or x is String, in the code that runs only when the check was true, the compiler treats x as the checked type, so x.length needs no !! and no as. It works in the branches of if and when, after an early return such as if (x !is Int) return, and on the right of &&. The compiler makes one only when the value cannot change between the check and the use, so it refuses on a var property, which other code could assign in between, and on a property with a custom getter, which may return something else on the next read. The usual fix is to copy the property into a local val, then check and use the copy.

  2. Fresher to mid

    The compiler refuses a smart cast on order.note, a var property, and a colleague silences it with order.note!!.trim() right after the null check. You review the change: when can that line still crash, and what would you ask for instead?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    A partner's booking feed is parsed into a Map<String, Any?> before your code sees it. Design the function that turns one map into a Booking(pnr: String, seats: Int, coupon: String?). Where do is checks, as? and preconditions go, and what does the caller get for a missing or wrong field?

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    A shared helper fun isValidPnr(s: String?): Boolean is called before every use of s, yet the compiler still wants s!! afterwards, and the team has started writing it everywhere. Why does the compiler not see the check, and what would you change?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • value is T checks the type while the program runs and !is is its opposite; for null, a check against a non-null type is false.
  • After a check that was true, the compiler smart-casts the value to the checked type in the branch, after an early return and on the right of &&.
  • value as? T gives null for any other type and pairs with ?:, while value as T throws a ClassCastException; neither one parses text.

Terms in this lesson

safe cast
A cast written with as?: x as? T gives x as a T when it is one, and null otherwise, instead of throwing a ClassCastException.
smart cast
The compiler's own cast: after a check such as x != null or x is String, it treats x as the checked type wherever the check still holds, with no cast written.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.