# Control Flow
Deft's control-flow forms sit naturally beside bare function calls
(see [Syntax and Style](syntax-and-style)). This page covers
conditionals, loops, iteration over collections, and `defer` for
cleanup.
## `if` / `else`
### Single-line form (preferred for simple conditions)
if ($n == 0) { return 1 } if logged-in? { set msg "Welcome!" } set msg [if admin? "Welcome, admin!" "Welcome, user!"]
### Multi-line form (complex logic)
if logged-in? { "Welcome!" } else { if admin? { "Welcome, admin!" } else { "Welcome, user!" } }
### Conditions
Conditions are **infix expressions in parentheses** — or a bare
truthy/falsy value. Bare parens `(...)` are NOT function calls (see
[Syntax and Style](syntax-and-style)).
if ($count > 10) { ... } # ✓ infix condition if logged-in? { ... } # ✓ bare predicate if $user~active { ... } # ✓ field access if [== $n 0] { ... } # ✗ invalid — `==` is infix-only, use ($n == 0)
### Truthiness
`nil` and `false` are falsy. Everything else (including `0`, `""`,
`@{}`, `%{}`) is truthy.
if "" { echo "truthy" } # prints: truthy if 0 { echo "truthy" } # prints: truthy if nil { echo "nope" } # no output
## `match`
Pattern matching dispatches on shape, type, and contents in one
expression. See [Pattern Matching](pattern-matching) for the complete
reference.
def classify {|n^number| match $n { 0 => "zero" 1 .. 9 => "single digit" $x when ($x < 0) => "negative" _ => "positive" } }
## `for` — Loop Over a Collection
`for x in $xs { ... }` iterates a list, map, set, string, or any
`ISeq`:
for x in @{ 1 2 3 } { echo $x } # 1 # 2 # 3 for entry in %{ :a 1 :b 2 } { echo "$entry~0 -> $entry~1" # map yields @{key value} pairs } # a -> 1 # b -> 2 for ch in "abc" { echo $ch } # string yields single chars
### `break` and `continue`
for x in @{ 1 2 3 4 5 } { if ($x == 2) { continue } if ($x == 4) { break } echo $x } # 1 # 3
### Index binding
for x idx in $xs { echo "[$idx] $x" }
### `range` for numeric ranges
for i [range 0 10] { echo $i } # 0..9 for i [range 1 10 2] { echo $i } # 1 3 5 7 9
## `while` — Loop With a Condition
set n 0 while ($n < 5) { echo $n incr n }
`while` works best for unbounded iteration (parsing input, polling,
backoff). For collection iteration, prefer `for`.
## `each` — Side-Effecting Walk
`each` is the functional form of `for`. Use it inside pipelines:
@{ 1 2 3 } |> each { echo $it }
%{ :a 1 :b 2 } |> each { echo "$it~0 = $it~1" }
Returns `nil`. Don't confuse it with `map` (which produces a lazy seq).
## `defer` — Cleanup on Exit
`defer` schedules a block to run when the enclosing function returns,
whether normally or via an early `return`/thrown error. Defers run in
last-in-first-out order.
def with-file {|path^string| set fh [fs/open $path "r"] defer { fs/close $fh } # ... use $fh ... # fs/close runs when the function exits, however it exits. }
### LIFO ordering
def ordered {| defer { echo "first" } defer { echo "second" } defer { echo "third" } "done" | } ordered # prints: third, second, first
### Early-return execution
def parse-config {|path^string| set raw [fs/read $path] defer { echo "parsed $path" } # runs even on early return if (empty? $raw) { return %{} } [json/parse $raw] }
### Scope independence
A `defer` inside a nested `def` only fires when that nested function
returns — defers are tied to the function frame, not the block:
def outer {| defer { echo "outer exits" } def inner {|| defer { echo "inner exits" } "inner" | } [inner] "outer" | } # inner exits # outer exits
See also [Concurrency](concurrency) for `defer`'s role around async
resources.
## `try` / `catch` — Error Handling
try { set result [fs/read $path] [json/parse $result] } catch e { echo "failed: $e~message" %{} }
The caught value is a tagged `%Err{ :message ... }` map. See
[Error Handling](error-handling) for the full Result story including
`ok`/`err`/`?`/`unwrap`.
## `throw` — Explicit Error
def divide {|a b| if ($b == 0) { [throw "division by zero"] } ($a / $b) }
The thrown value becomes the `%Err{ :message ... }` payload caught by
`try`.
## Early `return`
`return` exits the enclosing function immediately with the given value:
def find-first-positive {|xs^list| for x in $xs { if ($x > 0) { return $x } } nil }
`return` inside a closure body inside a function still returns from
the enclosing **function**, not the closure — closures don't introduce
new return frames.
## Quick Reference
| Form | Use |
|---|---|
| `if cond { ... } else { ... }` | Conditional |
| `match expr { pattern => result ... }` | Shape-based dispatch |
| `for x in $xs { ... }` | Walk a collection |
| `for x idx in $xs { ... }` | Walk with index |
| `while cond { ... }` | Condition-controlled loop |
| `each` | Side-effecting functional walk (in pipelines) |
| `defer { ... }` | Schedule cleanup |
| `try { ... } catch e { ... }` | Error handling |
| `return value` | Early exit |
| `break` / `continue` | Loop control |
| `[compile-when cond body]` | Compile-time conditional — elides `body` if `cond` falsy (see [Macros](10-macros)) |