Upcoming and OnDemand Webinars View full list

Write Better Code Using Kotlin’s Require, Check and Assert

Jeremy Sherman

Good code makes its context plain. At a glance, you can see what it needs to succeed, and what happens when it does. Mastering Kotlin’s common language for codifying your functions’ assumptions and promises will help you write code you can change with confidence. You will catch any bugs sooner, and you will spend less time debugging.

A Common Language

Kotlin has three functions for capturing execution context:

  • require(Boolean) throws IllegalArgumentException when its argument is false. Use it to test function arguments.
  • check(Boolean) throws IllegalStateException when its argument is false. Use it to test object state.
  • assert(Boolean) throws AssertionError when its argument is false (but only if JVM assertions are enabled with -ea). Use it to clarify outcomes and check your work.

These functions give Kotlin programmers a common language. If you do not use these functions, you will probably reinvent them.

Bare exceptions and errors are little help. So each function has another variation.That variation takes a lazy message closure as final argument. Use that message to jumpstart debugging by reporting relevant values. You will see these variations used soon.

These three functions look very similar, but each has its specific purpose. Examples of using each alone, then all together, will make that clear.

Capture Assumptions

A function makes assumptions about:

  • Direct Inputs: These are function arguments. Maybe you need an Int to be non-negative. Maybe you need a File to be readable. Before you begin working with your arguments, check that they are valid with require.

  • Indirect Inputs: These are often object state. Sometimes certain functions only make sense to call if other functions have been called already. A socket needs to connect to a host before it makes sense to read from or write to it. You check these conditions using check.

Require Arguments Be Valid

To check assumptions about function arguments, use require:

fun activate(index: Int) {
  // Argument Assumption: |index| is a non-negative integer.
  require(index > 0) { "Int |index| must be non-negative. index=$index" }

  
}

fun load(from: File): String {
  // Argument Assumption: |from| is a readable file.
  require(from.canRead()) { "File |from| must be readable. file=$from canRead=${from.canRead()}" }

  
}

To check assumptions about things that are not function arguments, use check:

class Socket {
  var isConnected: Boolean = false
  var connectedHost: Host? = null

  fun connect(to: Host, result: (isConnected: Boolean) -> Void) {
    // Starting State Assumption: |this| is not already connected.
    check(!isConnected) {
      "|Socket.connect| cannot be called after a successful call to |Socket.connect|. "+
      "socket=$this to=$to connectedHost=$connectedHost"
    }

    
  }

  fun write(blocks: Blocks): Int {
    // Starting State Assumption: |this| is connected.
    check(isConnected) {
      "|Socket.connect| must succeed before |socket.write| can be called. "+
      "socket=$this blocks=$blocks"
    }

    
  }
}

Promise Results

We write code to do something. When that something is to return a value, our promise is the return type. But return types often do not tell the whole story. And when that something is to change other state, our promise is secret.

Check Your Work with Assert

assert verifies your function did its job:

fun activate(index: Int) {
  
  // Ending State Promise: The pump at |index| is now active.
  assert(pump[index].isActive) { "Failed to activate pump index=$index" }
}

Conclusion

Kotlin gives us tools to write clear code. Clear code says what it knows. It does not keep it secret.

You often use require, check and assert in the same places in a function:

fun anyFunction(arg: Arg): Result {
  // Starting State Assumption: XXX
  check(internalStateIsSane) {
    "Say what you expected. Log |this| and |args| as well as the failing internal state."
  }

  // Argument Assumption: XXX
  require(arg.isSane) {
    "Say what you expected. Log |arg| and the values used in the failed check."
  }

  

  // Ending State Promise: XXX
  assert(result.isSane) {
    "Say what you expected. Log |result| and the failed check's output."
  }
  result
}

As shown, the pattern is:

  • Before anything else, check the starting state with check. If any of these checks fails, the arguments do not even matter – the function should never have been called!

  • Next, check the function arguments with require. If an argument turns out to be invalid, it is best to catch that before changing anything or doing any other work, since the function call will fail anyway.

  • In the middle, do the actual work of your function.

  • Lastly, assert the function did what it was supposed to do. Sometimes that means checking that some objects have a new state. Sometimes that means checking that the return value is reasonable.

In all cases, write your failure message to jumpstart debugging. If your first question when a check fails will be, “What was the value of something?” then the message should answer that question.

Checking things can grow tiresome. The way out is more precise types: As Yaron Minsky says, “Make illegal states unrepresentable.” For example, a require(intValue >= 0) check can be eliminated by using a type whose values can only represent non-negative integers. But that is a topic for another day.

Curious to better know Kotlin? Our two-day Kotlin Essentials course delivers in spades, while our Android Essentials with Kotlin course will set you on the right path for Android development.

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project