I’ve been reading Effective Kotlin by Marcin Moskala, in order to improve my knowledge of the Kotlin programming language beyond what is taught by a multitude of tutorials online. By reading Effective Kotlin I can learn some advice from someone that has battle-tested Kotlin and can provide insights into features of the language that I have potentially missed.
One of those unknown features that really made sense to me was using require
and check
, two functions from Kotlin’s standard library, much more than what I was currently using - which was never. You can use both these functions to perform assertions on specific requirements and states that we wish to have at a specific time.
require
require
, along with requireNotNull
, can be used to state that a given condition must be true. In the case of require
, it will take a Boolean
type as an argument while, in the case of requireNotNull
, it will take a type as an argument. Both functions will throw an IllegalArgumentException
if the conditions are not met. This means that if one of these happen it will throw the exception: false
in require
or null
in requireNotNull
.
From require
’s official documentation:
fun getIndices(count: Int): List<Int> {
require(count >= 0) { "Count must be non-negative, was $count" }
// ...
return List(count) { it + 1 }
}
// getIndices(-1) // will fail with IllegalArgumentException
println(getIndices(3)) // [1, 2, 3]
In this example, if we try to call getIndices
with an index lower than 0, it will throw an IllegalArgumentException
. Using these functions is important to validate arguments that are passed to functions in an idiomatic way. Since validating arguments is considered a best practice, even if the arguments are thoroughly documented - to produce clean, maintainable and secure code - using these functions can help us achieve that.
check
check
, along with checkNotNull
, are functions that can be used to validate that a given condition is true (or different than null in checkNotNull
). Similar to the require
functions, check
will take a Boolean
as an argument while checkNotNull
will take a type as an argument. Both functions throw an IllegalStateException
if those conditions are not met. This means that if one of these happen it will throw the exception: false
in check
or null
in checkNotNull
.
From check
’s official documentation:
var someState: String? = null
fun getStateValue(): String {
val state = checkNotNull(someState) { "State must be set beforehand" }
check(state.isNotEmpty()) { "State must be non-empty" }
// ...
return state
}
// getStateValue() // will fail with IllegalStateException
someState = ""
// getStateValue() // will fail with IllegalStateException
someState = "non-empty-state"
println(getStateValue()) // non-empty-state
In this example, we can see usage of both check
and checkNotNull
. Before returning the value of state
it uses checkNotNull
to validate that someState
is not null and then it uses check
to validate that isNotEmpty()
doesn’t return false
. If any of these doesn’t meet the criteria, it will throw an IllegalStateException
. Again, this is a great, idiomatic way of performing validation on state in Kotlin.
Conclusion
Kotlin has the standard assert
function as well, used in most programming languages, but that function throws an AssertionError
exception. There might be times where using assert
is the appropriate thing to do but there are also times where leveraging require
and check
will be more appropriate. By using require
and check
we get three things:
- We are able to validate function arguments and state.
- We are able to throw particular exceptions depending on the case.
- We are able to write idiomatic Kotlin code that is clean and readable.
require
and check
also have different purposes than assert
, as their name indicates, which makes using assert
everywhere unnecessary.
Leveraging require
and check
will lead to more secure and clean code but it is not without its drawbacks. By using require
and check
prolifically, without considering where they are placed, it can lead to a codebase that is filled with blocks of code that use exceptions to control flow. These functions should be used but use them with caution, where it makes sense. Given that Kotlin has nullable types, it can be difficult to understand where it makes sense to return a nullable type or where it makes sense to throw an exception.
I would say that require
and check
should be used where it is impossible to continue flow if the conditions are not met. If there’s any way that the flow can continue then require
and check
are possibly not the best options. In the examples above, getIndices
cannot proceed with a negative value because it will only throw a different exception at a later stage, while getStateValue
can’t proceed because it won’t be able to return a valid state if the state is empty. In both these cases, it made sense to halt the flow and throw an exception, which is a brilliant use case for both these functions.