How do I use a ?= or :=? guard in a try, in both statement and expression form?

← Control Flow · Ref: Q1358

A try accepts a guard in its header (directly after 'try'), and has an expression form that yields a value via a returning variable. If the guard leaves its variable unset, the try body is skipped and the expression yields the returning variable's initial value.

TRY EXPRESSION WITH A ?= GUARD

  config as String?
  chosen <- try config ?= loadConfig()
    <- rtn as String: "default.yaml"
    rtn: config
  catch
    -> ex as Exception
    rtn: `error ${ex}`

The guarded assignment (?=) assigns loadConfig() to 'config' and checks it is SET. If unset, the body is skipped and 'chosen' is "default.yaml".

TRY STATEMENT WITH A :=? GUARD

  cached as String?
  try cached :=? loadConfig()
    stdout.println(`loaded ${cached}`)
  catch
    -> ex as Exception
    stdout.println(`failed: ${ex}`)

Assign-if-unset (:=?) only assigns when 'cached' is currently unset, then gates the body on the result.

GUARD POSITION

The guard is the pre-flow, written right after 'try' — 'try <guard>' — before the body, resource header ('-> r <- ...') and returning variable. catch and finally are unaffected.

See Q78 for try guards. See Q1157 for try guard expressions. See Q137 for try-with-resources. See Q1361 for guards across all expression-form constructs.

Example

defines module qa.controlflow.guardtryexpression

  defines function

    loadConfig()
      <- rtn as String: "prod.yaml"

  defines program

    GuardTryExpressionDemo()
      stdout <- Stdout()

      // try EXPRESSION with a ?= guard (guard right after 'try')
      config as String?
      chosen <- try config ?= loadConfig()
        <- rtn as String: "default.yaml"
        rtn: config
      catch
        -> ex as Exception
        rtn: `error ${ex}`
      stdout.println(`chosen config: ${chosen}`)

      // try STATEMENT with a :=? assign-if-unset guard
      cached as String?
      try cached :=? loadConfig()
        stdout.println(`loaded ${cached}`)
      catch
        -> problem as Exception
        stdout.println(`failed: ${problem}`)

Common mistakes

E02001 — The guard belongs in the header directly after 'try' (try config ?= loadConfig()), not as the first statement of the body. As a guard it gates the whole body on the guard variable being set. See ek9 -h E02001.

Incorrect:

chosen <- try
        config ?= loadConfig()
        <- rtn as String: "default.yaml"
        rtn: config

Correct:

chosen <- try config ?= loadConfig()
        <- rtn as String: "default.yaml"
        rtn: config
      catch
        -> ex as Exception
        rtn: `error ${ex}`
Other ways to ask this
  • Can a try/catch use a guarded assignment in its header?
  • try as an expression with a guard variable
  • guarded assignment with try catch
  • How do I combine a guard with a try that returns a value?

Coming from another language?

Java: try is a statement, cannot bind a guarded loop-scoped variable in its header, and cannot yield a value; finally cannot set a result. You would null-check before the try. EK9: try takes a guard (<-, ?=, :=?) right after 'try', has an expression form yielding the returning variable, and the guard can skip the body entirely when unset.

Keywords: catch, exception, try, isset, expression, ?=, :=?, guard, guarded, returning, flow, control