Hem’s if, match, try, and postfix catch are expression-valued forms. In the supported
positions listed below, the compiler preserves evaluation order and emits standard TypeScript control flow. For unsupported positions and alternatives, see language boundaries. Arrow bodies
can be expression-bodied or block-bodied, with the final block expression serving as the return
value.
Value-producing forms
if
Parentheses around the condition are not used, and braces are required. Both branches contribute
to the result type. A value-position if without else produces ?T because its false path has
no value.
let size = (n: number): string =>
if n > 1000 { "large" } else { "small" }
let label = (enabled: boolean): ?string =>
if enabled { "on" }In statement position, if emits a standard TypeScript if. A guard branch that diverges with
return, raise, throw, continue, or break needs no value, and an absent else does not
introduce a temporary variable.
match
match selects one arm without fallthrough and returns that arm’s value. Exhaustiveness and
redundancy are checked for closed types. Patterns, guards,
as-bindings, structural records, and tuple scrutinees are documented in pattern
matching.
type State = | Ready | Busy
let describe = (state: State): string => match state {
| Ready => "ready"
| Busy => "busy"
}An arm containing a diverging expression, such as raise, does not need to match the surrounding
type. Matching open structural objects requires a final _ arm; see the pattern reference for
coverage rules.
try and postfix catch
try { ... } catch pattern { ... } is an expression-valued protected region. Its final expression,
or the final expression of its handler, provides the value. Postfix catch works the same way
and attaches to any expression, including await. Failure semantics and arm forms are in errors and
failures.
Arrow and block expressions
Arrows are the only function form. An expression body returns its expression. A block body runs its
statements in order and returns its final expression; a block with no final expression returns
void according to its contextual TypeScript type.
let normalize = (text: string): string => {
let trimmed = text.trim()
trimmed
}The same block-tail rule applies inside a value-producing if, match arm, try, or catch
handler. Each branch is checked against the contextual type of the enclosing expression.
Statement-shaped control flow
The compiler emits standard statements whenever the source is in statement position. A guard is written cleanly in Hem and remains clear in emitted TypeScript:
error InvalidCount({ value: number })
let requirePositive = (value: number): number => {
if value <= 0 { raise InvalidCount({ value }) }
value
}The branch raises and the following expression supplies the successful value. The same rule
applies to return, throw, continue, and break: a diverging branch does not require a
dummy value or an extra else block.
let-else
let pattern = scrutinee else diverging-expression binds a refutable pattern or takes the
diverging else branch. The else branch must diverge with return, throw, raise, continue,
or break; a branch that produces an ordinary value causes HEM2014. The scrutinee is evaluated once,
and successful bindings are available after the statement.
let firstOrZero = (values: number[]): number => {
let Some(first) = values[0] else return 0
first
}The refutable pattern set includes Some, constructors, literals, records, arrays, and as
aliases. Omitting else on a root canonical Some, Ok, or native ?T binding propagates the
unchanged residual to the nearest arrow, async arrow, or Effect block. An Effect block receives it
on its success channel; postfix ! is the separate operation that converts a canonical carrier’s
residual into an Effect failure. See absence (?T) and
standard carriers.
for await
let printLines = async (lines: AsyncIterable<string>) => {
for await line in lines { console.log(line) }
}for await pattern in source lowers to native for await (const pattern of source).
It is legal inside async arrows and at module top level, wherever core await is legal.
Binding patterns, including destructuring and as aliases, and break/continue work as
in for; the loop is a statement and yields no value. A non-async arrow receives TypeScript’s
TS1103 diagnostic at the loop’s await keyword.
Ranges count synchronously: use for i in a..b (HEM3007). Inside effect {} or
resource {} blocks, await lifts a single Promise, so for await is refused (HEM4119).
Use Stream.fromAsyncIterable with Stream.runForEach, Effect.forEach over an array,
or an async helper arrow defined outside the block and lift its Promise with await.
while let
while let pattern = scrutinee { ... } is the loop form of a refutable binding. Hem re-evaluates the
scrutinee at the start of every iteration, stops when the pattern fails, and scopes the successful
bindings to that iteration. It has no else and produces no value.
let sum = (values: number[]): number => {
let remaining: mut number[] = [...values]
let mut total = 0
while let Some(value) = remaining.pop() {
total += value
}
total
}The loop pops from an explicitly mutable copy, leaving the caller’s values unchanged.
The pattern in the loop header must be refutable. A bare binding, _, or a record/array containing
only bindings always matches and triggers HEM2022; use a plain while condition instead. for
loops and ranges are also statements. Ranges such as 0..limit and 0..=limit are valid only in a
for loop header, not as standalone expressions.
Value-position limits
Hem lowers value-producing control flow into declared-then-assigned TypeScript while preserving
evaluation order. When a match, an if, or a statement block is the value a function returns,
each branch or arm returns directly instead, so no temporary appears. Nested try, match, and statement-bearing if expressions work in ordinary
method arguments and on the right of &&, ||, and ??. A skipped right operand stays skipped.
For a method call, the receiver and method are captured before the arguments, and this still
refers to the original receiver. Arguments evaluate from left to right.
Existing optional-call forms also accept these arguments. A short-circuited receiver or callable skips all argument work, including recovery. Parentheses retain their usual optional-chain boundary.
let risky = (): number => { throw new Error("unavailable") }
let recover = (): number => 3
let invoke = (callback: ?((value: number) => number)): ?number =>
callback?.(risky() catch { | _ => recover() })The catch protects only risky(). It does not catch failures from recover() or callback.
let hasText = (text: ?string): boolean =>
text != None && try { text.length > 0 } catch { false }
let fallbackCount = (count: ?number): number =>
count ?? try { 42 } catch { 0 }These limits remain:
- These hoists work at value boundaries such as initializers, return tails, and arm bodies.
Statement
if/whileconditions and match guards still reject nested control flow; compute a condition beforeif, or inside each loop iteration, rather than moving repeated work outside a loop. - Nested control flow in
supercalls and method-tagged templates is refused with HEM3003. Keep the conditional call explicit and bind its argument inside that branch. - Synchronous postfix
catchsupports the existing value-position paths, including optional-call arguments. Arbitrary standalone statement blocks do not support every value-position form. - Call-parameter contextual types are not passed into hoisted temporaries. If inference needs one,
use an annotated
letbefore the call. - Loops are statements and cannot be used as expressions.
while lethas noelseand no value.- A range is valid only in a
forheader, not as a standalone expression.
The compiler does not wrap standard code in an IIFE to work around these cases. The only exception is a control-flow expression inside JSX braces, where the emitted code must remain an expression; see the JSX reference.
Normative source
Specification 0.2, §4 (bindings), §5 (arrows), §6 (expressions), and §7 (control flow). The value-position lowering engine follows the return and hoisting rules in specification 0.2.