How do I handle multiple preconditions without early return?

← Control Flow Without break/continue/return · Ref: Q285

In Java you write: if (!valid1) return error1; if (!valid2) return error2; doWork();. EK9 has no return. Use structured if/else chains or decompose into validator functions.

THE PROBLEM

Early return guards look clean in small functions but create hidden exits. Each return is a path the reader must trace. Cleanup code can be skipped. In large functions, the number of hidden exits becomes unmanageable.

PATTERN 1: NESTED IF/ELSE CHAIN

Check each condition in sequence with all paths visible:

  validate()
    -> name as String, age as Integer, email as String
    <- message as String: "unknown error"
    if length name == 0
      message: "name is required"
    else if age < 0 or age > 150
      message: "age must be between 0 and 150"
    else if not (email contains "@")
      message: "email must contain @"
    else
      message: "valid"

Every path is explicit. No hidden exits.

PATTERN 2: VALIDATOR FUNCTIONS WITH GUARDS

Decompose each check into its own function:

  validateName() as pure
    -> name as String
    <- error as String: String()
    if length name == 0
      error: "name is required"

Then compose with guarded assignment:

  problem <- String()
  problem :=? validateName(name)
  problem :=? validateAge(age)
  problem :=? validateEmail(email)

The :=? means the first error wins. Subsequent checks are still evaluated but their results are ignored once problem is set.

PATTERN 3: VALIDATE THEN PROCESS

Separate validation from processing entirely:

  isValid() as pure
    -> name as String, age as Integer
    <- valid as Boolean: length name > 0 and age >= 0 and age <= 150

Guard on the result:

  if isValid(name, age)
    process(name, age)
  else
    reportError()

KEY INSIGHT

Early return creates hidden exits. Explicit if/else makes ALL paths visible. Decomposition into validator functions keeps each check focused and testable.

See Q50 for declared returns. See Q274 for AI return patterns. See Q146 for decomposition. See Q74 for guard expressions.

Example

defines module qa.without.preconditions

  defines function

    // Pattern 1: Nested if/else chain
    validateAll() as pure
      ->
        name as String
        age as Integer
        email as String
      <- message as String: "unknown error"
      maxAge <- 150
      if length name == 0
        message: "name is required"
      else if age < 0 or age > maxAge
        message: "age must be between 0 and 150"
      else if not (email contains "@")
        message: "email must contain @"
      else
        message: "valid"

    // Pattern 2: Individual validators
    validateName() as pure
      -> name as String
      <- error as String: String()
      if length name == 0
        error: "name is required"

    validateAge() as pure
      -> age as Integer
      <- error as String: String()
      maxAge <- 150
      if age < 0 or age > maxAge
        error: "age must be between 0 and 150"

    validateEmail() as pure
      -> email as String
      <- error as String: String()
      if not (email contains "@")
        error: "email must contain @"

    // Pattern 3: Boolean validator
    isAllValid() as pure
      ->
        name as String
        age as Integer
      <- valid as Boolean?
      maxAge <- 150
      valid: length name > 0 and age >= 0 and age <= maxAge

  defines program

    PreconditionDemo()
      stdout <- Stdout()

      // === PATTERN 1: NESTED IF/ELSE ===

      testName <- "Alice"
      testEmail <- "a@b.com"

      stdout.println(`Empty name: ${validateAll("", 25, testEmail)}`)
      stdout.println(`Bad age: ${validateAll(testName, -5, testEmail)}`)
      stdout.println(`Bad email: ${validateAll(testName, 25, "nope")}`)
      stdout.println(`All valid: ${validateAll(testName, 25, testEmail)}`)

      // === PATTERN 2: COMPOSED VALIDATORS ===

      problem <- String()
      problem :=? validateName(testName)
      problem :=? validateAge(200)
      problem :=? validateEmail(testEmail)
      if problem?
        stdout.println(`Composed error: ${problem}`)

      problem2 <- String()
      problem2 :=? validateName("Bob")
      problem2 :=? validateAge(30)
      problem2 :=? validateEmail("b@c.com")
      if ~problem2?
        stdout.println("Composed: all valid")

      // === PATTERN 3: VALIDATE THEN PROCESS ===

      if isAllValid("Charlie", 40)
        stdout.println("Charlie is valid, processing")
      else
        stdout.println("Charlie is invalid")

      if isAllValid("", 40)
        stdout.println("Empty name is valid")
      else
        stdout.println("Empty name rejected as expected")

Common mistakes

E50001 — A pure function cannot call non-pure methods like stdout.println(). Pure functions must have no side effects. See ek9 -h E50001 for details.

Incorrect:

validateName() as pure
      -> name as String
      <- error as String: String()
      stdout.println(name)
      if length name == 0
        error: "name is required"

Correct:

validateName() as pure
      -> name as String
      <- error as String: String()
      if length name == 0
        error: "name is required"
Other ways to ask this
  • How do I validate multiple conditions without return in EK9?
  • What replaces guard clauses with early return in EK9?
  • How do I write a validation chain without return statements in EK9?

Coming from another language?

Java: if (!valid) return error; guard clauses at top of method. Python: if not valid: return error early return guards. Rust: early return with ? operator for Result, pattern matching for validation. Go: if err := validate(); err != nil { return err } very common pattern. Kotlin: require() and check() for preconditions, early return. Swift: guard let for unwrapping, early return. EK9: no return, nested if/else chains, validator functions with :=? composition, validate-then-process decomposition.

Keywords: chain, precondition, no-return, migrate, null-safe, guard, return, early, check, no-break, validation, validate, safe, multiple, isset, error, condition, alternative