DEV Community

Trevor Nemanic
Trevor Nemanic

Posted on • Originally published at trevornemanic.com

Beginner Kotlin Runtime Check Tutorial: Require, Check, Assert

We often assume the state of our program is acceptable when our functions are called. Sometimes we assume the state or input can be invalid, so we do something like the following:

fun doThing(foo: Bar?) {
    if (foo == null) {
        return
    }
    .... // what you meant to do
}

Both these assumptions can lead to hard-to-find errors and unreadable code. Fortunately, the Kotlin standard library provides three basic functions for ensuring everything is in order:

  1. Require: My function needs parameters to be valid in this way. Throw an IllegalArgumentException when false.
  2. Check: My function's dependency is in the correct state to be called. Throw an IllegalStateException when false.
  3. Assert: My function's result is valid. Throw an AssertionError when false. Also, runtime assertions have to be enabled on the JVM with -ea or enabled with Native.

In addition to validation of a condition, each one of these functions has an optional lazy argument for an error message if evaluated to false. All three runtime check APIs are the same. Here is an example from require.

fun require(value: Boolean)
inline fun require(value: Boolean, lazyMessage: () -> Any)

Let's take a look at each runtime check in depth, then write code to fire a laser cannon

Require

Require is used for parameter validation. It ensures its parameter is in a valid state.

fun fireLaser(cannonNumber: Int) {
    require(cannonNumber >= 0) { "Must pass a valid cannon number: $cannonNumber < 0" }
    .... // fire laser beams!!!
}

Check

Check makes sure our function's dependencies can be called safely. It is used to verify everything is ready to go.

fun fireLaser() {
    // setup above
    check(laserCannonManager.isReadyToFire) { "You must arm the lasers before firing!" }
    .... // fire laser beams!!!
}

Assert

Assert does the last bit of validation before a function is done or returns. It is your last chance before handing control to another part of the program.

fun fireLaser() {
    // setup above
    val fired = laserCannon.fire()
    assert(fired == true) { "cannon did not fire successfully" }
}

Why runtime checks?

They provide runtime assurance which can be useful in addition to unit tests. The checks force the program to fail fast and give the developer more info than a stacktrace from a distant failed component.

Obviously, these checks shouldn't be everywhere and should be added judiciously. Crucial functions which exist in a complicated space would benefit the most. See below how all three runtime checks ensure proper usage of our core laser cannon functionality.

fun fireLaser(cannonNumber: Int) {

    // first, validate parameters
    require(cannonNumber >= 0) { "Must pass a valid cannon number"}

    // then, verify working dependencies
    check(laserCannonManager.isReadyToFire) { "You must arm the lasers before firing!" }
    check(guidanceSystem.canLockOn()) { "Cannot lock on target" }

    // call laser code safely
    laserCannonManager.charge(cannonNumber)
    guidanceSystem.lockOn()
    val laserCannon = laserCannonManager.cannons[cannonNumber]
    val fired = laserCannon.fire() // fire laser beams!!!

    // lastly, make sure our cannon fired flawlessly
    assert(fired == true) { "cannon did not fire successfully" }

}

Hopefully you have a comfortable understanding of these runtime conditions. Feel free to post any questions you have in the comments!
Did you like what you read? You can also follow me on Twitter for Kotlin and Android content.

Top comments (0)