How does the Optional type work in EK9?

← Getting Started · Ref: Q47

EK9 has a generic Optional type that replaces null with compiler-enforced safety. The compiler tracks whether the '?' (isSet) operator has been called and REFUSES to compile code that accesses an Optional without a proper guard. There are no escape hatches: no .unwrap(), no !!, no force-unwrap.

CREATING OPTIONALS

Two forms:

  maybe <- Optional() of Integer      empty, must specify type
  item <- Optional(42)                with value, type inferred

CHECKING WITH ?

The ? operator checks if an Optional has a value:

  if item?
    value <- item.get()               safe: ? in same if block

The verbose form 'item is not empty' also works but ? is preferred.

DECLARATION GUARD

Combine creation with safety check in one expression:

  if o <- getOptional()
    value <- o.get()                  safe: declaration implies ? check

The variable 'o' only exists inside the if block. If unset, the block is skipped entirely.

GET OR DEFAULT

Extract the value with a fallback:

  value <- o.getOrDefault("default")

Returns the contained value if set, otherwise returns the default. No guard needed.

THINK EK9 — NOT JAVA, NOT RUST

  WRONG (Java):    if (opt.isPresent()) opt.get()
  WRONG (Kotlin):  opt?.let { use(it) } ?: default()
  WRONG (Rust):    match opt { Some(v) => v, None => default }
  RIGHT (EK9):     if opt? then use(opt.get())

See Q84 for Optional operations (comparison, copy, merge, ternary guard). See Q85 for Optional in stream pipelines (flatten, collect). See Q74-Q78 for guard patterns in all flow controls (if, switch, for, while, try). See Q48 for the Result type (success-or-error container). See Q29 for tri-state semantics. See Q54 for Consumer and Acceptor callbacks. See Q126 for choosing the right collection type (List, Dict, Optional, Result, PriorityQueue). See Q198 for built-in generics. See Q243 for coalescing operators (??, ?:) that work with Optional values.

Use 'ek9 -h Optional' to see the full API. See Q251 for fixing unset variable compiler errors. See Q316 for how discarded Optional returns are compiler errors. See Q634 for guard-based safe access. See Q636 for chained guard access. See Q747 for migrating Swift optional binding chains to EK9.

Example

defines module qa.optional

  defines function

    <?-
      Returns an Optional with a value.
    -?>
    getOptional()
      <- rtn <- Optional("Steve")

    <?-
      Returns an empty Optional.
    -?>
    getEmptyOptional()
      <- rtn <- Optional() of String

  defines program
    OptionalBasics()
      stdout <- Stdout()

      // === CREATING OPTIONALS ===

      // Empty — must specify type
      maybe <- Optional() of Integer
      stdout.println(`Empty isSet: ${maybe?}`)

      // With value — type inferred
      item <- Optional(42)
      stdout.println(`Item: ${item}, isSet: ${item?}`)

      // === DECLARATION GUARD ===

      // Preferred: combines creation + safety check
      if o <- getOptional()
        extracted <- o.get()
        stdout.println(`Guard: ${extracted}`)

      // Empty Optional — guard block not entered
      if empty <- getEmptyOptional()
        stdout.println("Should not print")

      // Explicit ? check form
      o2 <- getOptional()
      if o2?
        checked <- o2.get()
        stdout.println(`Checked: ${checked}`)

      // === GET OR DEFAULT ===

      name <- getOptional()
      assured <- name.getOrDefault("Default")
      stdout.println(`GetOrDefault (set): ${assured}`)

      emptyName <- getEmptyOptional()
      defaulted <- emptyName.getOrDefault("Fallback")
      stdout.println(`GetOrDefault (empty): ${defaulted}`)

Common mistakes

E08030 — Calling .get() on an Optional without first checking with ? triggers E08030 — has not been checked before access. The compiler enforces guard-before-access with no escape hatches. See ek9 -h E08030 for details.

Incorrect:

checked <- o2.get()
      stdout.println(`Checked: ${checked}`)

Correct:

if o2?
        checked <- o2.get()
        stdout.println(`Checked: ${checked}`)

E11052 — Calling a function that returns Optional but discarding the result triggers E11052 — Result or Optional result is discarded and must always be checked. You must always handle the return value. See ek9 -h E11052 for details.

Incorrect:

getOptional()
      assured <- getOptional().getOrDefault("Default")

Correct:

name <- getOptional()
      assured <- name.getOrDefault("Default")
Other ways to ask this
  • How do I handle missing values safely in EK9?
  • How does EK9 prevent null pointer exceptions?
  • What is Optional in EK9?
  • What replaces null in EK9?
  • What is the EK9 equivalent of Rust Option?

Coming from another language?

Java: Optional<T> with .get() that throws NoSuchElementException if empty, .isPresent() check not enforced by compiler, .orElse() for defaults. Python: no Optional type, uses None with 'is None' checks, no compile-time safety. Rust: Option<T> with .unwrap() that panics if None, pattern matching for safe access, compiler enforces match exhaustiveness but allows .unwrap() escape hatch. Go: no Optional, uses comma-ok idiom and nil checks. Kotlin: nullable types T? with safe call ?. and Elvis ?:, !! force-unwrap throws NPE. Swift: T? optional with if let/guard let for safe unwrapping, ?? nil coalescing for defaults, ! force-unwrap crashes at runtime (escape hatch EK9 does not allow), optional chaining with ?. for method calls. EK9: Optional of T with compiler-enforced guard patterns, .get() requires ? check in same control structure, no escape hatches, ZERO runtime crashes from Optional misuse.

Keywords: guard, safe, first, safety, isSet, compiler, unset, migrate, get, beginner, optional, start, empty, replace, nullable, nil, null-safe, none, isset, getOrDefault, npe, set, absent, enforce, null, swift, intro