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