JavaRush /Courses /Kotlin SELF /Preconditions and fail-fast: require/check and guard clau...

Preconditions and fail-fast: require/check and guard clauses

Kotlin SELF
Level 14, Lesson 4
Available

1. Why it’s sometimes better to crash immediately than to pretend everything is fine

If you’ve ever seen a bug that shows up “once every three weeks on Thursdays, but only if the Moon is in Capricorn,” then you already know the core problem: somewhere long ago the program went off the rails, but it kept running, pretending everything was fine. Fail-fast is the habit of stopping execution immediately once it’s clear that continuing no longer makes sense. It’s like a fire alarm: it keeps you from sleeping, but it clearly explains that sleeping won’t be happening anyway.

In our input/parsing/validation utilities we’ve already chosen a soft strategy: for expected data errors we return null. But there’s another class of problems: incorrect function usage. For example, you wrote a range validator, and someone (often you) accidentally passed min=100, max=10. This is not “the user typed something wrong,” it’s “the programmer mixed up the bounds.” And here null is a poor fit: it makes the error quiet.

Let’s lock in the difference:

Situation Example Normal reaction in our case
Expected data problem (user input) Entered "qwerty" instead of a number Return null; the scenario will explain it and decide what to do
Precondition violation (programmer error) Passed a range where min > max Fail-fast via require/check

That’s exactly why Kotlin has the “precondition functions” require() and check(): they automatically throw an exception and stop execution when the condition is not met.

2. Preconditions: what they are

A precondition is a rule that must be true before the main logic of a function starts, otherwise the function is not required to work correctly. This is not about “checking absolutely everything,” but about protecting against an incorrect call. And an important point: preconditions are not “user input validation,” because the user does not call your functions directly; they enter text, and you decide what to do with it.

It’s convenient to phrase a precondition as if you were writing a short note to your future self: “This function works only if …”. And if the condition isn’t satisfied, let the program crash immediately—loudly and with a clear message. It’s unpleasant, but very honest: “I can’t continue because the input conditions are wrong.”

Two types of preconditions: arguments and state

Preconditions are often split into two kinds, and Kotlin deliberately provides two different functions for them. The boundary isn’t mathematically perfect, but in real life it helps a lot to keep a consistent style in your head and not turn checks into a mess.

Preconditions on arguments are when the function received parameters and you want to make sure they are valid: for example, min <= max, “discount from 0 to 100,” “size > 0.” For this you typically use require(...), and on violation it throws IllegalArgumentException.

Preconditions on state are when the arguments look fine, but there is some internal program state, and the function assumes it has already been prepared: “the session has started,” “settings are loaded,” “call init first.” For this you typically use check(...), and on violation it throws IllegalStateException.

3. require: validate arguments and crash with IllegalArgumentException

require(condition) { message } is the standard way to say: “You are calling the function incorrectly because the arguments don’t fit.” In this case Kotlin throws IllegalArgumentException. This is exactly the situation where the error should be as visible as possible: if the arguments are wrong, the problem is in the code, not in the user.

Important: require is not a tool for “the user typed letters instead of digits.” For that, we already have …OrNull and handling at the scenario level. require is a tool for “the programmer passed nonsense.”

Range validator with protection against min > max

We already wrote validators like “check that a number is within a range.” Now let’s strengthen them: we’ll add a precondition that the range itself is specified correctly.

fun validateIntInRangeOrNull(x: Int, min: Int, max: Int): Int? {
    require(min <= max) { "Expected min <= max, but got min=$min, max=$max" }

    if (x < min) return null
    if (x > max) return null
    return x
}

Notice the subtle but important philosophy: if x “doesn’t fit,” that’s a normal data situation, so we return null. But if the range is absurdly defined, that’s not “data,” it’s “someone (possibly me) made a mistake in the code,” so we fail-fast.

Utility to read a number within a range

Let’s add one more “scenario-level” function that combines reading and validation. Here require is especially useful because we pass the range bounds not by the user, but by the programmer.

fun readIntOrNull(prompt: String): Int? {
    print(prompt)
    val s = readln()
    return s.toIntOrNull()
}

fun readIntInRangeOrNull(prompt: String, min: Int, max: Int): Int? {
    require(min <= max) { "Expected min <= max, but got min=$min, max=$max" }

    val x = readIntOrNull(prompt) ?: return null
    return validateIntInRangeOrNull(x, min, max)
}

If someone calls readIntInRangeOrNull("Age: ", 150, 0), the program will crash immediately—and that’s good. Because otherwise you’ll get “weird” scenarios where it keeps returning null, and you start suspecting the user, the keyboard, and retrograde Mercury.

4. check: validate state and crash with IllegalStateException

check(condition) { message } is a way to say: “I expect the program to be in a valid state internally right now; otherwise this is a logic error.” In this case Kotlin throws IllegalStateException. If require more often catches a “bad call from the outside,” then check catches “we backed ourselves into a corner.”

In beginner-level console programs, state often looks like one or two variables: “settings are set,” “the user name has already been entered,” “a mode is selected.” That’s already enough to see the value of check.

Example: the session must be started

Let’s make a very tutorial-ish but illustrative example. Imagine we have a mini program where you must first “start a session,” and only then ask questions.

var sessionStarted: Boolean = false

fun startSession() {
    sessionStarted = true
}

fun askUserName(): String {
    check(sessionStarted) { "Session not started. Call startSession() first." }

    print("Your name: ")
    return readln()
}

Here the user isn’t at fault at all. They don’t even know what startSession() is. This is our internal protocol. If we violate the call order, that’s a logic error, and it’s better to see it immediately.

Why not require here

We could write require(sessionStarted), and technically it would also crash. But the meaning would be worse: IllegalArgumentException hints “you provided bad arguments,” while the problem isn’t in the arguments, but in the fact that “the program is in the wrong state.” check expresses this more precisely, and precision is half of good diagnostics.

Messages in require/check: write them so you won’t be embarrassed later

The message in require/check is not “decoration,” it’s half the value. If you write just require(min <= max), then on failure you’ll get an exception but without context: what exactly was passed? In small programs you can sometimes guess, but in real ones you’ll thank yourself for the details.

A good message usually contains two parts: “what was expected” and “what was received.” It sounds pedantic, but it’s the kind of pedantry that saves hours.

fun validateIntInRangeOrNull(x: Int, min: Int, max: Int): Int? {
    require(min <= max) { "Expected min <= max, but got min=$min, max=$max" }

    if (x < min) return null
    if (x > max) return null
    return x
}

If it crashes, you’ll immediately see the specific numbers. This is especially important when ranges aren’t “hard-coded,” but computed somewhere above (even if from other variables).

5. Guard clauses: how to stop writing an if pyramid

Imagine typical beginner code (and not only beginner code, honestly): check a condition, inside another check, inside another… You end up with a “pyramid” that drifts to the right and causes headaches. Guard clauses are a style where you do checks up front and exit the function immediately if something is wrong. The code becomes linear: it reads top-down like a list of obstacles on the way to the main logic.

Important: guard clauses are not a separate language construct. It’s simply the habit of writing if (...) return ... instead of nested blocks.

Before: nesting and a staircase to the right

Suppose we want to read the width and height of a rectangle (1..100) and compute the area. Here’s a version that makes your eyes start looking for the “Undo” button:

fun rectangleAreaScenario() {
    val w = readIntOrNull("Width: ")
    if (w != null) {
        val h = readIntOrNull("Height: ")
        if (h != null) {
            if (w in 1..100) {
                if (h in 1..100) {
                    println("Area = ${w * h}")
                } else {
                    println("Height is out of range.")
                }
            } else {
                println("Width is out of range.")
            }
        } else {
            println("Height is not a number.")
        }
    } else {
        println("Width is not a number.")
    }
}

The code is technically correct, but reading it is like opening a matryoshka doll: until you open all the layers, you can’t see where the area is.

After: guard clauses and early exit

Now let’s rewrite it so the main intent (“compute the area”) becomes visible quickly, and the checks don’t build a pyramid.

fun rectangleAreaScenario() {
    val w = readIntOrNull("Width (1..100): ") ?: run {
        println("Width must be an integer.")
        return
    }
    if (w !in 1..100) {
        println("Width is out of range 1..100.")
        return
    }

    val h = readIntOrNull("Height (1..100): ") ?: run {
        println("Height must be an integer.")
        return
    }
    if (h !in 1..100) {
        println("Height is out of range 1..100.")
        return
    }

    println("Area = ${w * h}") // for example: Area = 12
}

Notice how pleasant it is to read now: if something is wrong, we exit immediately and explain it. If we made it to the end, everything is fine and we compute the area.

How guard clauses work together with …OrNull and require/check

It’s easy to get confused here, so let’s fix the idea with a small diagram. We use null and guard clauses when “we failed to obtain a correct value.” We use require/check when “the function was called/used incorrectly.”

flowchart TD
    A[Function start] --> B{Is the problem expected?}
    B -->|User input, format, range| C[Return null / exit via guard clause]
    B -->|Function preconditions violated| D[require/check -> exception]
    C --> E[Scenario decides what to print]
    D --> F[Bug in code is visible immediately]

And yes: fail-fast does not cancel careful handling of null. These are two tools for two different kinds of problems.

6. Building fail-fast into a mini API: carefully but confidently

Now we’ll take a small step toward more “adult” code: our utilities will not only return null for expected problems, but also protect themselves against incorrect usage. We still stay within a single lesson: no input retry loops and no complex architecture—just discipline.

Let’s assemble a mini set of functions (simplified, but coherent) and use it in main().

Normal composition: reading + validation + guard clauses

fun readIntOrNull(prompt: String): Int? {
    print(prompt)
    return readln().toIntOrNull()
}

fun validateIntInRangeOrNull(x: Int, min: Int, max: Int): Int? {
    require(min <= max) { "Expected min <= max, but got min=$min, max=$max" }

    if (x < min) return null
    if (x > max) return null
    return x
}

fun readIntInRangeOrNull(prompt: String, min: Int, max: Int): Int? {
    require(min <= max) { "Expected min <= max, but got min=$min, max=$max" }

    val x = readIntOrNull(prompt) ?: return null
    return validateIntInRangeOrNull(x, min, max)
}

Note: we intentionally duplicate require(min <= max) in two functions. Yes, it’s repetition. But it’s “local insurance”: if you decide to change one function or use the other independently, each is protected. In practice you can refactor later, but right now clarity matters more.

Mini scenario in main(): area

fun main() {
    val w = readIntInRangeOrNull("Width (1..100): ", 1, 100) ?: run {
        println("Invalid width.")
        return
    }
    val h = readIntInRangeOrNull("Height (1..100): ", 1, 100) ?: run {
        println("Invalid height.")
        return
    }

    val area = w * h
    println("Area = $area") // for example: Area = 12
}

It’s not the app of the century, but it perfectly demonstrates the style: utilities are “quiet,” the scenario is “talkative,” and preconditions protect us from silly calls.

Example of check: initialization is mandatory

Let’s add a mini example of state. Imagine we want to configure the bounds once, and then read values using those bounds. We’re not doing “real configuration” here; we’re just illustrating the idea of check.

var configuredMin: Int? = null
var configuredMax: Int? = null

fun configureLimits(min: Int, max: Int) {
    require(min <= max) { "Expected min <= max, but got min=$min, max=$max" }
    configuredMin = min
    configuredMax = max
}

fun readConfiguredIntOrNull(prompt: String): Int? {
    check(configuredMin != null && configuredMax != null) {
        "Limits are not configured. Call configureLimits(min, max) first."
    }

    val min = configuredMin!!
    val max = configuredMax!!
    return readIntInRangeOrNull(prompt, min, max)
}

Yes, there is !! here, and in an ideal world we would avoid it. But at this level, what matters is this: check guarantees that configuredMin and configuredMax are already set; otherwise the program will crash immediately with a clear message. In teaching examples this is a normal illustration of “state,” and later we’ll learn to design things so that !! is needed less often.

7. Common mistakes

Mistake #1: using require/check for ordinary invalid user input.
It’s very tempting to write require(x in 1..100) when the user enters 200. But then the program will crash on any “wrong” input, and the user will feel like they’re testing your nerves. For input we use a different style: …OrNull and handling at the scenario level.

Mistake #2: “swallowing” precondition violations via null.
If your function should only be called with valid parameters (for example, min <= max), returning null is a bad idea. You’ll turn an explicit error into a silent one, and later you’ll spend a long time looking for why “it keeps returning null.” These situations should crash immediately via require or check.

Mistake #3: writing require(condition) without a message.
Technically it works, but diagnostically it’s almost useless. The message should explain the expectation and show the actual values. A week later you won’t remember what exactly you were checking, but min=$min, max=$max will help.

Mistake #4: confusing require and check and losing the meaning of the exception.
If you’re checking argument correctness, that’s usually require and IllegalArgumentException. If you’re checking internal state, that’s usually check and IllegalStateException. Kotlin explicitly positions these functions as different in meaning and in the exception type.

Mistake #5: building an if pyramid instead of using guard clauses, and then being afraid to touch the code.
Nested if quickly turns a function into a labyrinth where it’s easy to forget which branch relates to what. Guard clauses with early return make the code linear: “if it’s not right—exit; otherwise keep working.” This improves readability and reduces the chance of logical errors during edits.

Mistake #6: doing checks but continuing execution “hoping for the best.”
Sometimes people write something like if (x == null) println("Error"), but they don’t do return, and then the code proceeds with null or with an invalid value. Guard clauses are valuable precisely because after the message you actually exit, rather than continuing to program in “maybe we’ll get lucky” mode.

1
Task
Kotlin SELF, level 14, lesson 4
Locked
Checkout discount
Checkout discount
1
Task
Kotlin SELF, level 14, lesson 4
Locked
Operator session
Operator session
1
Task
Kotlin SELF, level 14, lesson 4
Locked
Area without a staircase
Area without a staircase
1
Task
Kotlin SELF, level 14, lesson 4
Locked
Letter Grade by Score
Letter Grade by Score
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION