How do guard variables work in try blocks?

← Control Flow · Ref: Q78

In EK9, guard variables in try blocks combine variable creation with an isSet check before the try statement runs. If the guard is unset, the WHOLE try statement is skipped: the body, catch AND finally. The guard gates the statement exactly as it gates an if, switch, for or while.

BASIC TRY GUARD

Use the declaration operator (<-) to guard a try block:

  try resource <- acquireResource()
    useResource(resource)
  catch
    -> ex as Exception
    handleError(ex)
  finally
    cleanup()

If acquireResource() returns unset, the whole statement is skipped: the body does not run, the catch block does not fire (no exception occurred), and the finally block does not run either. Put cleanup that must happen whatever the guard gives outside the guarded try.

GUARD VS EXCEPTION

This is the key distinction:

  Guard unset: Resource unavailable. Whole try statement skipped, finally included. No error.
  Exception: Resource acquired but fails during use. Catch handles it.

Guard failures are expected situations (optional resource not available). Exceptions are unexpected errors during processing.

AN EXCEPTION FROM THE GUARD ITSELF

The guard runs once, OUTSIDE the try body. If acquireResource() itself throws, this try's catch does not see it and its finally does not run: the exception goes to the ENCLOSING handler (an outer try, or the caller). Wrap the guarded try in another try when the acquisition itself can fail.

TRY EXPRESSION WITH GUARD

Try can be an expression that returns a value:

  result <- try config <- loadConfig()
    <- rtn as String: "default"
    rtn: config.host
  catch
    -> ex as Exception
    rtn: "error: " + $ex

If the guard is unset, the expression evaluates to the default return value.

WHEN FINALLY RUNS

Once the guard is set and the try statement is entered, the finally block always executes, whether the body completes or throws - the same guarantee as Java's finally. When the guard is unset the statement is never entered, so its finally does not run.

See Q74 for guards in if. See Q75 for guards in switch. See Q76 for guards in for loops. See Q77 for guards in while loops. See Q134 for basic try/catch without guards. See Q135 for try/catch/finally. See Q137 for try-with-resources.

Example

defines module qa.flow.guard.tryblock

  defines record

    DbConnection
      url <- String()

      default operator ?

  defines function

    openConnection()
      <- rtn <- DbConnection()
      rtn.url: "jdbc:example:db"

    openFailedConnection()
      <- rtn <- DbConnection()

  defines program

    GuardTryDemo()
      stdout <- Stdout()

      // === BASIC TRY GUARD (resource available) ===

      try conn <- openConnection()
        stdout.println("Connected to: " + conn.url)
      catch
        -> ex as Exception
        stdout.println("Error: " + $ex)
      finally
        stdout.println("Cleanup after connection attempt")

      // === TRY GUARD WITH UNSET (the whole try is skipped, finally included) ===

      try conn <- openFailedConnection()
        stdout.println("Body: never printed")
      catch
        -> ex as Exception
        stdout.println("Catch: never printed")
      finally
        stdout.println("Finally: never printed")
      stdout.println("After the unset guard: whole try skipped")

      // === TRY EXPRESSION WITH GUARD ===

      result <- try conn <- openConnection()
        <- rtn as String: "no connection"
        rtn: "connected to " + conn.url
      catch
        -> ex as Exception
        rtn: "error"

      stdout.println("Result: " + result)

      // === TRY EXPRESSION WITH UNSET GUARD (the default return value) ===

      fallback <- try conn <- openFailedConnection()
        <- rtn as String: "no connection"
        rtn: "connected to " + conn.url

      stdout.println("Fallback: " + fallback)

Common mistakes

E01072 — EK9 has no return statement. Try guards combine resource acquisition and isSet checking. If the guard is unset, the try body is skipped safely without needing an early return. See ek9 -h E01072 for details.

Incorrect:

conn <- openConnection()
      if not conn?
        return
      try
        stdout.println("Connected to: " + conn.url)
      catch
        -> ex as Exception
        stdout.println("Error: " + $ex)

Correct:

try conn <- openConnection()
        stdout.println("Connected to: " + conn.url)
      catch
        -> ex as Exception
        stdout.println("Error: " + $ex)

E01072 — EK9 has no return statement. Try expressions use a declared return variable that is assigned in the body and catch branches. See ek9 -h E01072 for details.

Incorrect:

try conn <- openConnection()
        return "connected to " + conn.url

Correct:

result <- try conn <- openConnection()
        <- rtn as String: "no connection"
        rtn: "connected to " + conn.url
Other ways to ask this
  • Can I guard a try block with a variable declaration?
  • How does try with guard work in EK9?
  • What happens if a try guard is unset?
  • Does finally run when a try guard is unset?
  • How do I use try-catch in EK9?
  • How does exception handling work in EK9?

Coming from another language?

Java: No guard in try. Try-with-resources handles cleanup but cannot skip the try body if resource is unavailable. Must wrap: 'var r = acquire(); if (r != null) { try { use(r); } catch ... }'. Python: No guard in try. 'with' statement handles cleanup but cannot conditionally skip. Must wrap: 'r = acquire(); if r: try: use(r)'. Rust: No guard in try (Rust uses Result/Option instead of try/catch). Pattern matching with '?' operator is different. Go: No try/catch. Defers handle cleanup. Guard concept does not apply. EK9: 'try resource <- acquireResource()' combines resource acquisition, isSet check, and exception handling. Guard failure is silent (no exception) and skips the whole try statement, finally included; once entered, finally always runs.

Keywords: acquire, finally, try, null-safe, guard, migrate, handle, declaration, condition, error, catch, isset, control, safe, flow, resource, exception, branch