---
title: "Expressions and control flow"
description: "Expression-valued branches, block tails, guarded bindings, loops, and value-position limits."
image: "https://hem.run/social/hem.png"
---

> Documentation Index
> Fetch the complete documentation index at: https://hem.run/llms.txt
> Use this file to discover all available pages before exploring further.

# Expressions and control flow

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](/reference/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.

```hem
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](/reference/pattern-matching).

```hem
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](/reference/typed-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.

```hem
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:

```hem
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.

```hem
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`)](/reference/absence) and
[standard carriers](/reference/standard-carriers).

## `for await`

```hem
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.

```hem
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.

```hem
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`.

```hem
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`/`while` conditions and match guards still reject nested control flow; compute a
  condition before `if`, or inside each loop iteration, rather than moving repeated work outside a loop.
- Nested control flow in `super` calls and method-tagged templates is
  refused with HEM3003. Keep the conditional call explicit and bind its argument inside that branch.
- Synchronous postfix `catch` supports 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 `let` before the call.
- Loops are statements and cannot be used as expressions.
- `while let` has no `else` and no value.
- A range is valid only in a `for` header, 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](/reference/jsx).

## 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.

Source: https://hem.run/reference/control-flow/index.mdx
