Control Flow

Both if and for are expressions in Parsley — they return values. There are no while loops, no switch/match statements, and no ternary operator (because if already fills that role).

if / else

if evaluates a condition using truthiness and returns the value of the taken branch. Parentheses around the condition are optional when using braces.

// Compact form (parens required without braces)
let status = if (age >= 18) "adult" else "minor"

// Block form (parens optional)
if x > 0 {
    "positive"
} else {
    "non-positive"
}

// Chained
let label = if (x < 0) "negative" else if (x == 0) "zero" else "positive"

Without an else, a false condition returns null:

let x = if (false) "yes"         // null

⚠️ There is no ternary operator (? :). Use if (cond) a else b — it's an expression that works anywhere a value is expected.

for

for maps over an iterable and returns an array. Null results are automatically filtered out, which gives you map and filter in one construct.

Map

for (x in [1, 2, 3]) { x * 10 }           // [10, 20, 30]
for (n in 1..5) { n * n }                  // [1, 4, 9, 16, 25]

Parentheses are optional when using braces:

for x in [1, 2, 3] { x * 10 }             // [10, 20, 30]

Filter

Return a value to keep it; let the block evaluate to null (by not entering the if) to discard it:

for (x in [1,2,3,4,5]) { if (x > 3) { x } }  // [4, 5]

Map + Filter

Combine transformation and filtering in one loop:

for (x in 1..10) {
    if (x % 2 == 0) { x * 100 }
}
// [200, 400, 600, 800, 1000]

With Index

Two-variable form gives you the index (0-based) and the element:

for (i, x in ["a", "b", "c"]) { i + ": " + x }
// ["0: a", "1: b", "2: c"]

Over Dictionaries

Iterate over key-value pairs. One variable gets the value; two variables get the key and value:

for (k, v in {a: 1, b: 2, c: 3}) { k + "=" + v }
// ["a=1", "b=2", "c=3"]

Over Strings

Iterates over individual characters:

for (ch in "abc") { ch }                   // ["a", "b", "c"]

Over Tables

Iterates over rows (as dictionaries or records). See Data Model.

Loop Control

stop

Exits the loop early (like break in other languages). Results collected so far are returned:

for (x in [1,2,3,4,5]) {
    if (x == 3) { stop }
    x
}
// [1, 2]

skip

Skips the current iteration (like continue). The current element produces no result:

for (x in [1,2,3,4,5]) {
    if (x % 2 == 0) { skip }
    x
}
// [1, 3, 5]

⚠️ stop and skip can only be used inside for loops. Using them elsewhere is a runtime error.

check

A guard expression for preconditions. If the condition is falsy, the else value is returned as the function's result (early exit):

let validate = fn(x) {
    check x > 0 else "must be positive"
    check x < 100 else "must be under 100"
    "ok: " + x
}
validate(50)    // "ok: 50"
validate(-1)    // "must be positive"
validate(200)   // "must be under 100"

check is useful for validation at the top of a function — it avoids deeply nested if/else chains. The else value becomes the function's return value immediately.

try

Wraps a function or method call and catches recoverable errors. Returns a dictionary with result and error keys:

let risky = fn() { fail("oops") }

let outcome = try risky()
// {result: null, error: {message: "oops", code: "USER-0001"}}

let safe = fn() { 42 }
let outcome = try safe()
// {result: 42, error: null}

Destructure for clean error handling:

let {result, error} = try risky()
if (error) {
    "Failed: " + error.message
} else {
    "Got: " + result
}

⚠️ try only wraps function and method calls — not arbitrary expressions. try 1 + 2 is a parse error.

Catchable vs Non-Catchable Errors

try only catches errors from external/runtime factors:

Catchable Non-catchable
IO, Network, Database Type, Arity, Parse
Format, Value, Security Undefined, Operator, State, Import

Non-catchable errors (logic bugs) propagate through try and halt execution. This is intentional — a type mismatch shouldn't be silently swallowed.

fail

Creates a catchable error. Accepts a string message or a dictionary with structured error data. Useful for signalling application-level error conditions that callers can handle with try:

// String form — wraps in {message: ..., code: "USER-0001"}
fail("division by zero")

// Dictionary form — must have a string "message" key
fail({message: "Out of stock", code: "NO_STOCK", status: 400})

Example with check guard:

let divide = fn(a, b) {
    check b != 0 else fail("division by zero")
    a / b
}

let {result, error} = try divide(10, 0)
error.message                               // "division by zero"

fail creates a Value-class error, which is always catchable by try. The error slot in the try result is a dictionary — use error.message to get the message string. String coercion also works: "" + error yields error.message.

with

The with expression creates a scoped context where dictionary fields become directly accessible as variables. This reduces repetition when working with structured data.

let person = {name: "Alice", age: 30, city: "NYC"}

with person {
    `{name} is {age} years old`
}
// "Alice is 30 years old"

Inside the with block, dictionary keys are bound as local variables. The block returns the value of its last expression.

Syntax

Parentheses are optional:

// Without parentheses
with {x: 10, y: 20} { x + y }   // 30

// With parentheses (useful for complex expressions)
with ({x: 10, y: 20}) { x + y } // 30

Use Cases

Reduce repetition when accessing nested data:

let order = {
    customer: "Alice",
    items: ["Widget", "Gadget"],
    total: 99.99
}

with order {
    <div class="receipt">
        <h2>"Order for " customer</h2>
        <p>"Total: $" total</p>
    </div>
}

Cleaner template rendering:

let data = {title: "Welcome", body: "Hello, world!"}

with data {
    <article>
        <h1>title</h1>
        <p>body</p>
    </article>
}

Scope

Variables defined outside with are still accessible. If a dictionary key shadows an outer variable, the dictionary value takes precedence inside the block:

let name = "Outer"
let data = {name: "Inner", value: 42}

with data {
    name                // "Inner" (shadowed)
}
name                    // "Outer" (unchanged)

⚠️ with only works with dictionaries. Passing a non-dictionary value is a runtime error.

Key Differences from Other Languages

See Also