What is the require statement and when should I use it?

← Error Handling and Exceptions · Ref: Q304

The require statement validates preconditions in production code. When a require condition is false or unset, it throws an uncatchable exception that terminates execution immediately. This is similar to panic in Go or Rust.

KEY CHARACTERISTICS

- Uncatchable: require failures cannot be caught with try/catch. There is no recovery.
- Always active: unlike Java assert which can be disabled, EK9 require is always checked.
- For programming errors: use require when a failure means the caller has a bug.
- Fail-fast: detects contract violations immediately rather than allowing corrupt state.

WHEN TO USE REQUIRE

Use require for:
- Null/unset parameters when the method cannot accept unset values.
- Collection must not be empty (require not items.empty()).
- Method called in wrong object state (require status == OrderStatus.Created).
- Internal invariants that should never be violated.

WHEN NOT TO USE REQUIRE

Do not use require for:
- User input validation (use catchable exceptions or Result instead).
- File not found or network timeout (these are recoverable).
- Any condition that could reasonably fail due to external factors.

REQUIRE VS THROW

require: For programming errors and contract violations. Uncatchable. Similar to panic.
throw: For exceptional but potentially recoverable situations. Catchable with try/catch.

See Q134 for try/catch. See Q139 for error handling strategy. See Q305 for how require differs from assert. See Q136 for throwing exceptions.

Example

defines module qa.errorhandling.preconditions

  defines type

    OrderStatus
      Created
      Submitted
      Shipped

  defines class

    <?-
      Demonstrates require for precondition checking.
      Require validates that callers honour the contract.
    -?>
    Order
      items as List of String: List() of String
      status as OrderStatus: OrderStatus.Created

      <?-
        Constructor with preconditions.
        The caller must provide valid, non-empty data.
      -?>
      Order()
        ->
          customerId as String
          initialItems as List of String
        require customerId?
        require initialItems?
        require not initialItems.empty()
        this.items :=: initialItems

      <?-
        Can only add items while order is in Created state.
        Calling this after submit is a programming error.
      -?>
      addItem()
        -> item as String
        require item?
        require status == OrderStatus.Created
        items += item

      <?-
        Submit the order. Must have items and be in Created state.
      -?>
      submit()
        require not items.empty()
        require status == OrderStatus.Created
        status: OrderStatus.Submitted

      <?-
        Ship the order. Must be in Submitted state.
      -?>
      ship()
        require status == OrderStatus.Submitted
        status: OrderStatus.Shipped

      itemCount() as pure
        <- rtn as Integer: length items

      override operator ? as pure
        <- rtn as Boolean: items? and status?

      operator $ as pure
        <- rtn as String: `Order[${status}, ${itemCount()} items]`

  defines class

    <?-
      Demonstrates require for validating arguments
      in a utility class.
    -?>
    PriceCalculator

      <?-
        Calculate total price. Both arguments must be set and valid.
      -?>
      calculateTotal()
        ->
          unitPrice as Float
          quantity as Integer
        <-
          rtn as Float: Float()

        require unitPrice?
        require quantity?
        require unitPrice >= 0.0
        require quantity >= 0

        rtn: unitPrice * Float(quantity)

  defines program

    RequirePreconditionsDemo()
      stdout <- Stdout()

      stdout.println("=== Order lifecycle with require ===")

      initialItems <- List() of String
      initialItems += "Widget"
      initialItems += "Gadget"

      order <- Order("CUST-001", initialItems)
      stdout.println("Created: " + $order)

      order.addItem("Sprocket")
      stdout.println("Added item: " + $order)

      order.submit()
      stdout.println("Submitted: " + $order)

      order.ship()
      stdout.println("Shipped: " + $order)

      stdout.println("=== Price calculation with require ===")

      calc <- PriceCalculator()
      total <- calc.calculateTotal(9.99, 3)
      stdout.println("Total: " + $total)

Common mistakes

E50001 — Renaming the variable means later references to 'order' become unresolved, triggering E50001. Variable names must be consistent. See ek9 -h E50001 for details.

Incorrect:

orderXYZ <- Order("CUST-001", initialItems)

Correct:

order <- Order("CUST-001", initialItems)
Other ways to ask this
  • How do I enforce preconditions in EK9?
  • What is the EK9 equivalent of assert or panic?
  • How do I validate method arguments in EK9?

Coming from another language?

Go: panic() is uncatchable unless recover() is used. Rust: panic!() unwinds the stack and terminates the thread. Java: assert can be disabled at runtime with -da flag (weak). C/C++: assert() can be compiled out with NDEBUG (weak). Python: assert can be disabled with -O flag. Kotlin: require() throws IllegalArgumentException (catchable, unlike EK9). EK9: require is always active, always uncatchable, stronger than all of these.

Keywords: exception, safe, migrate, handle, require, guard, invariant, argument, uncatchable, catch, precondition, isset, panic, validate, null-safe, contract