The Loup Web Book — contents
  1. Chapter 1: Getting Started
  2. Chapter 2: The Web Modes
  3. Chapter 3: Variables and Data Types
  4. Chapter 4: Functions
  5. Chapter 5: Control Flow
  6. Chapter 6: Collections
  7. Chapter 7: Structs
  8. Chapter 8: Enums and Pattern Matching
  9. Chapter 9: Classes and Objects
  10. Chapter 10: Interfaces
  11. Chapter 11: Generics
  12. Chapter 12: Error Handling
  13. Chapter 13: Modules and Packages
  14. Chapter 14: Template Fences — HTML and CSS
  15. Chapter 15: Async and Concurrency
  16. Chapter 16: Reactive UI
  17. Chapter 17: Web Components
  18. Chapter 18: DOM, Storage, Workers, and JavaScript Interop
  19. Chapter 19: SSR, Hydration, and Deployment
  20. Chapter 20: Memory Safety in Web Apps

Chapter 5: Control Flow

What you'll learn. if / else if / else as statements and expressions, while, do/while, the two for loops, labeled break and continue, match, the cleanup tools (try/finally, defer, with), and the propagation operators ? and attempt.

Why it matters. Most of Loup's control flow looks like C, TypeScript or Python, but a handful of rules differ: every condition is parenthesized, there is no ternary operator, elif is not a statement keyword, and match is checked for exhaustiveness. Knowing these up front saves a round of compiler errors.

The listings in this chapter are @mode(web.compiled) programs. As Chapter 2 explains, loup run calls main() for you in that mode, so every program has one.

if, else if and else

@mode(web.compiled)

func classify(celsius: int): string {
    if (celsius < 0) {
        return "freezing";
    } else if (celsius < 15) {
        return "cold";
    } else if (celsius < 25) {
        return "mild";
    } else {
        return "hot";
    }
}

func main(): void {
    for (t in [-5, 8, 20, 31]) {
        println(i"{$ t $}°C is {$ classify(t) $}");
    }
}

Listing 5-1: An else if chain

-5°C is freezing
8°C is cold
20°C is mild
31°C is hot

Two rules are visible here. Every condition sits inside parentheses (ruling PAREN-1), and the chain is spelled else if. Python programmers tend to reach for elif, which Loup only knows inside template directives (Chapter 14):

This code does not compile
@mode(web.compiled)

func classify(celsius: int): string {
    if (celsius < 0) {
        return "freezing";
    } elif (celsius < 15) {
        return "cold";
    } else {
        return "warm";
    }
}

func main(): void {
    println(classify(4));
}
main.lpweb:6:27: error[E0003]: expected `;`
main.lpweb:8:7: error[E0001]: expected expression
main.lpweb:8:12: error[E0003]: expected `;`
main.lpweb:10:5: error[E0001]: expected expression
main.lpweb:11:1: error[E0001]: expected expression

The parser reads elif as an ordinary name, so the messages are about a missing ; rather than about elif. When an if chain produces a burst of "expected ;" errors, look for an elif. Writing } else if (celsius < 15) { fixes it, as in Listing 5-1.

The parentheses are required even around a simple comparison:

This code does not compile
@mode(web.compiled)

func main(): void {
    let stock = 3;
    if stock < 5 {
        println("reorder soon");
    }
}
main.lpweb:5:8: error[E0001]: E_CONDITION_PARENS: the condition of `if` must be in parentheses; add them: `if (...) {`

This message names the fix. The same rule applies to while conditions and for headers.

Conditions must be bool

Loup has no truthiness: 0, "" and an empty list are not false. The Language Reference (§1-6.3) makes a non-bool condition the compile error E_CONDITION_NOT_BOOL. Loup 1.25.1 does not check this yet, so the mistake only surfaces when the program runs:

This code throws at run time
@mode(web.compiled)

func main(): void {
    let pending = 3;
    if (pending) {
        println("work to do");
    }
}
Uncaught RuntimeError: internal interpreter limitation: type error: CondBranch on non-bool: Int(3)

Write the comparison you mean, if (pending > 0) { … }.

if as an expression

An if with an else produces a value. The value of each branch is its last expression, written without a semicolon:

@mode(web.compiled)

func main(): void {
    let items = 7;
    let label = if (items == 1) { "item" } else { "items" };
    println(i"{$ items $} {$ label $}");

    let delta = -4;
    let sign = if (delta > 0) { 1 } else if (delta < 0) { -1 } else { 0 };
    println(sign);
}

Listing 5-2: if expressions

7 items
-1

There is no ?: ternary operator (ruling SQ-3). A TypeScript habit gives an unhelpful message:

This code does not compile
@mode(web.compiled)

func main(): void {
    let items = 7;
    let label = items == 1 ? "item" : "items";
    println(label);
}
main.lpweb:5:30: error[E0003]: expected `;`

A ? after an operand is always the propagation operator (covered at the end of this chapter), so the parser stops at the string that follows it. Use the if expression from Listing 5-2.

The Language Reference requires the else branch on an if expression, because every path must produce a value. Loup 1.25.1 does not enforce this yet and gives the missing branch an empty value, so always write the else.

Loops

while and do/while

@mode(web.compiled)

func main(): void {
    var n = 3;
    while (n > 0) {
        println(i"{$ n $}...");
        n = n - 1;
    }
    println("liftoff");

    var attempts = 0;
    do {
        attempts = attempts + 1;
        println(i"attempt {$ attempts $}");
    } while (attempts < 2);
}

Listing 5-3: while and do/while

3...
2...
1...
liftoff
attempt 1
attempt 2

A do/while runs its body at least once and ends with a semicolon after the condition.

for over a collection or a range

The for (x in source) form visits each element of a list, a set, a dict's keys, or the range form start..end:

@mode(web.compiled)

func main(): void {
    let fruits = ["apple", "banana", "cherry"];
    for (fruit in fruits) {
        println(fruit);
    }

    for (i in 0..3) {
        println(i"row {$ i $}");
    }

    let prices = {"apple": 120, "banana": 45};
    for ((name, cents) in prices.entries()) {
        println(i"{$ name $} costs {$ cents $} cents");
    }

    for (var i = 0; i < 10; i = i + 4) {
        println(i);
    }
}

Listing 5-4: for over a list, a range and dict entries, and the counted for

apple
banana
cherry
row 0
row 1
row 2
apple costs 120 cents
banana costs 45 cents
0
4
8

The range 0..3 visits 0, 1 and 2: the end is exclusive and there is no inclusive form. A range is part of the loop header, not a value (ruling SQ-4), so it cannot be stored or passed:

This code does not compile
@mode(web.compiled)

func main(): void {
    let firstThree = 0..3;
    for (i in firstThree) {
        println(i);
    }
}
main.lpweb:4:22: error[E0001]: E_RANGE_FORM_MISPLACED: the range form `start..end` is permitted only as the iteration source of a `for` loop or a comprehension clause (§1-6.4)

Write for (i in 0..3) directly, or build a list if you need the numbers as data.

The loop variable is immutable. Assigning it is a mistake carried over from languages where the loop variable is an ordinary variable:

This code does not compile
@mode(web.compiled)

func main(): void {
    let prices = [100, 250, 80];
    for (p in prices) {
        p = p + p // 10;
        println(p);
    }
}
main.lpweb:6:9: error[E0001]: cannot assign to immutable binding 'p'

Declare the loop variable with var when the body needs to change its copy. The change never reaches the list:

@mode(web.compiled)

func main(): void {
    let prices = [100, 250, 80];
    for (var p in prices) {
        p = p + p // 10;
        println(p);
    }
    println(prices);
}

Listing 5-5: A mutable loop variable is a fresh copy each iteration

110
275
88
[100, 250, 80]

(// is integer floor division in Loup; comments start with #.)

Do not add to or remove from a collection while a for loop walks it. The Language Reference makes that the compile error E_BORROW_LOOP_MUTATION; Loup 1.25.1 does not check it, and on the interpreter a push inside such a loop can keep the loop running forever. Collect the changes in a second list and apply them after the loop.

Labeled break and continue

A label names a loop, so break or continue can target an outer loop from inside an inner one:

@mode(web.compiled)

func main(): void {
    let shelves = [[4, 0, 7], [3, 9], [0, 0, 2]];
    var checked = 0;
    shelves: for (shelf in shelves) {
        for (count in shelf) {
            checked = checked + 1;
            if (count == 0) {
                continue shelves;
            }
            if (count > 8) {
                println(i"overstocked shelf found after {$ checked $} bins");
                break shelves;
            }
        }
    }
}

Listing 5-6: Labeled loops

overstocked shelf found after 4 bins

The label shelves lives in its own namespace, so it does not clash with the variable shelves. Misspelling a label is a compile error:

This code does not compile
@mode(web.compiled)

func main(): void {
    rows: for (row in [[1, 2], [3, 4]]) {
        for (cell in row) {
            if (cell == 2) {
                continue row;
            }
        }
    }
}
main.lpweb:7:17: error[E0001]: continue label 'row' not found

match

match compares a value against patterns in order and evaluates the first arm that fits. It is an expression, and its arms are separated by commas:

@mode(web.compiled)

func describe(n: int): string {
    return match n {
        0 => "none",
        1 => "one",
        k if (k < 0) => "invalid",
        k if (k < 10) => "a few",
        _ => "many",
    };
}

func main(): void {
    for (n in [0, 1, -4, 7, 99]) {
        println(i"{$ n $}: {$ describe(n) $}");
    }

    let status = 404;
    match status {
        200 => println("ok"),
        404 => {
            println("not found");
            println("check the URL");
        },
        _ => println("unexpected status"),
    }
}

Listing 5-7: Literal, binding, guard and wildcard patterns

0: none
1: one
-4: invalid
7: a few
99: many
not found
check the URL

The patterns used so far:

  • a literal such as 0 or 404;
  • a binding such as k, which matches anything and names it;
  • a guard, k if (k < 0), which adds a condition in parentheses;
  • the wildcard _, which matches anything without naming it;
  • an arm body can be a single expression or a { … } block.

The scrutinee is written without parentheses: match n { … }.

Exhaustiveness

When you match on an enum, the compiler checks that every variant is handled. Chapter 8 covers enums in depth; this is the check you will meet most often:

This code does not compile
@mode(web.compiled)

enum Light { Red, Yellow, Green }

func next(light: Light): Light {
    return match light {
        Light.Red => Light.Green,
        Light.Green => Light.Yellow,
    };
}

func main(): void {
    println(next(Light.Red));
}
main.lpweb:6:12: error[E0001]: Non-exhaustive match: missing variants Yellow

The message lists every missing variant. Add the arm:

@mode(web.compiled)

enum Light { Red, Yellow, Green }

func next(light: Light): Light {
    return match light {
        Light.Red => Light.Green,
        Light.Green => Light.Yellow,
        Light.Yellow => Light.Red,
    };
}

func main(): void {
    println(next(Light.Red));
    println(next(Light.Yellow));
}

Listing 5-8: An exhaustive match over an enum

Green
Red

The Language Reference requires every match to be exhaustive. For an enum, Loup 1.25.1 enforces that at compile time; for an open type such as int or string it does not, and a value that no arm accepts raises MatchError when the program runs:

This code throws at run time
@mode(web.compiled)

func describe(n: int): string {
    return match n {
        0 => "none",
        1 => "one",
    };
}

func main(): void {
    println(describe(5));
}
Uncaught MatchError: No matching arm for value in match expression
  at describe (main.lpweb:4:12)
  at main (main.lpweb:11:13)

End a match over an open type with a _ arm, as Listing 5-7 does.

Tuple and type patterns

A tuple pattern matches element by element. Literals and bindings mix freely:

@mode(web.compiled)

func locate(point: (int, int)): string {
    return match point {
        (0, 0) => "origin",
        (x, 0) => i"on the x axis at {$ x $}",
        (0, y) => i"on the y axis at {$ y $}",
        (a, b) if (a == b) => "on the diagonal",
        _ => "somewhere else",
    };
}

func main(): void {
    for (p in [(0, 0), (3, 0), (0, -2), (4, 4), (1, 5)]) {
        println(locate(p));
    }
}

Listing 5-9: Tuple patterns

origin
on the x axis at 3
on the y axis at -2
on the diagonal
somewhere else

⚠ Known issue in 1.25.1. If a later tuple-pattern arm binds the same name as an earlier one, for example (x, 0) => … followed by (x, y) if (x == y) => …, the earlier arm sees None for that name on the interpreter. Listing 5-9 uses different names (a, b) in the guard arm to avoid it.

A value whose type is a union, such as int | string, is matched with a type pattern, int(n), that both checks the type and binds the value:

@mode(web.compiled)

func show(value: int | string): string {
    return match value {
        int(n) => i"number {$ n $}",
        string(s) => i"text '{$ s $}'",
    };
}

func main(): void {
    println(show(42));
    println(show("forty-two"));
}

Listing 5-10: Matching a union type

number 42
text 'forty-two'

No OR-patterns

Loup's pattern grammar has no | alternative (Language Reference §1-19.6), so a Rust-style 'a' | 'e' => arm does not parse:

This code does not compile
@mode(web.compiled)

func kind(letter: string): string {
    return match letter {
        "a" | "e" | "i" | "o" | "u" => "vowel",
        _ => "consonant",
    };
}

func main(): void {
    println(kind("e"));
}
main.lpweb:5:13: error[E0001]: expected FatArrow, found Pipe
main.lpweb:5:13: error[E0001]: expected expression
main.lpweb:5:19: error[E0001]: expected FatArrow, found Pipe
main.lpweb:5:19: error[E0001]: expected expression
main.lpweb:5:25: error[E0001]: expected FatArrow, found Pipe
main.lpweb:5:25: error[E0001]: expected expression
main.lpweb:5:31: error[E0001]: expected FatArrow, found Pipe
main.lpweb:5:31: error[E0001]: expected expression

Use a guard instead:

@mode(web.compiled)

func kind(letter: string): string {
    return match letter {
        l if ("aeiou".contains(l)) => "vowel",
        _ => "consonant",
    };
}

func main(): void {
    println(kind("e"));
    println(kind("k"));
}

Listing 5-11: A guard in place of an OR-pattern

vowel
consonant

Reserved words are not names

Some ordinary-looking words are reserved (Language Reference §1-3.4). where, for example, introduces generic constraints, so it cannot name a variable:

This code does not compile
@mode(web.compiled)

func main(): void {
    let where = "warehouse 3";
    println(where);
}
main.lpweb:4:9: error[E0001]: `where` is a reserved word (§1-3.4) and cannot name a binding
  note: rename the binding — e.g. `where_value`

Pick another name, such as location.

Cleaning up: try/finally, defer and with

try, catch and finally

Loup has exceptions. A catch clause names the exception type, then as, then the binding. finally runs whether the try body finished, threw, or was handled:

@mode(web.compiled)

func average(total: int, count: int): int {
    return total // count;
}

func main(): void {
    for (count in [4, 0]) {
        try {
            println(i"average: {$ average(100, count) $}");
        } catch (IntegerDivisionError | ValueError as e) {
            println(i"skipped: {$ e.message $}");
        } finally {
            println(i"done with count {$ count $}");
        }
    }
}

Listing 5-12: try / catch / finally

average: 25
done with count 4
skipped: integer division or modulo by zero
done with count 0

catch (A | B as e) catches either type. Clauses are tried in order, so put specific types before general ones such as Exception. Chapter 12 covers the exception hierarchy.

The catch syntax is Type as name, not the name: Type form of TypeScript or Kotlin:

This code does not compile
@mode(web.compiled)

func main(): void {
    try {
        throw ValueError("bad quantity");
    } catch (e: ValueError) {
        println(e.message);
    }
}
main.lpweb:6:15: error[E0001]: expected RParen, found Colon
main.lpweb:6:15: error[E0002]: expected `{`
main.lpweb:6:15: error[E0001]: expected expression
main.lpweb:6:17: error[E0003]: expected `;`
main.lpweb:9:1: error[E0001]: expected expression

The first line is the one that matters: the parser wanted ) where it found :. Write catch (ValueError as e).

A finally block cannot change where control goes. return, break or continue inside it is E_FINALLY_CONTROL.

defer

defer registers a statement or block to run when the enclosing block ends, however it ends. Several defers run in last-in, first-out order (Language Reference §1-6.5).

⚠ Known gap in 1.25.1. defer compiles, but the deferred code never runs, on the interpreter or in a Wasm build. Until that is fixed, use try { … } finally { … } for cleanup that must happen.

This listing shows the gap. By the specification it would print cleanup 2 and cleanup 1 after working:

This code does not do what it should
@mode(web.compiled)

func work(): int {
    defer { println("cleanup 1"); }
    defer { println("cleanup 2"); }
    println("working");
    return 42;
}

func main(): void {
    println(work());
}
working
42

The same cleanup written with finally runs today:

@mode(web.compiled)

func work(): int {
    try {
        println("working");
        return 42;
    } finally {
        println("cleanup");
    }
}

func main(): void {
    println(work());
}

Listing 5-13: finally as the working substitute for defer

working
cleanup
42

with and Scoped[T]

with (name = expr) { … } brackets a block with setup and teardown. The value of expr must implement Scoped[T]: the runtime calls scope_enter() and binds its result to name, runs the block, then calls scope_exit(error), passing None or the exception that is leaving the block (Language Reference §1-6.6). Returning false from scope_exit lets an exception continue on its way.

@mode(web.compiled)

class Timer implements Scoped[string] {
    pub let label: string;
    new(label: string) { this.label = label; }

    pub func scope_enter(): string {
        println(i"start {$ this.label $}");
        return this.label;
    }

    pub func scope_exit(error: Option[Any]): bool {
        println(i"stop {$ this.label $}");
        return false;
    }
}

func main(): void {
    with (name = Timer("render")) {
        println(i"working in {$ name $}");
    }

    try {
        with (name = Timer("save")) {
            throw ValueError("disk full");
        }
    } catch (ValueError as e) {
        println(i"caught: {$ e.message $}");
    }
}

Listing 5-14: A Scoped resource

start render
working in render
stop render
start save
stop save
caught: disk full
main.lpweb:12:39: warning: W_ANY_IN_AOT_MODE: use of `Any` in web.compiled mode prevents AOT specialisation — consider a static type if performance is critical
main.lpweb:12:39: warning: W_ANY_IN_AOT_MODE: use of `Any` in web.compiled mode prevents AOT specialisation — consider a static type if performance is critical

The stop save line appears before the exception reaches the catch: teardown runs even when the block throws. The warnings come from the Option[Any] parameter that the Scoped protocol specifies. W_ANY_IN_AOT_MODE is an advisory, not an error (Chapter 2); the Language Reference exempts this one parameter from it, but 1.25.1 still prints it.

A with on a value that is not Scoped is rejected:

This code does not compile
@mode(web.compiled)

class Timer {
    pub let label: string;
    new(label: string) { this.label = label; }
}

func main(): void {
    with (t = Timer("render")) {
        println("working");
    }
}
main.lpweb:9:5: error[E0001]: E_WITH_NOT_SCOPED: type 'Timer' does not implement Scoped[T] — a `with` resource must provide scope_enter() and scope_exit(error) (Closeable and Releasable do)
main.lpweb:9:5: error[E0001]: E_WITH_NOT_SCOPED: type 'Timer' does not implement Scoped[T] — a `with` resource must provide scope_enter() and scope_exit(error) (Closeable and Releasable do)
main.lpweb:9:5: error[E0001]: E_WITH_NOT_SCOPED: type 'Timer' does not implement Scoped[T] — a `with` resource must provide scope_enter() and scope_exit(error) (Closeable and Releasable do)

(1.25.1 prints the same diagnostic three times.) Add implements Scoped[…] and the two methods, as in Listing 5-14.

Propagating errors: ? and attempt

Functions that can fail without it being exceptional return Result[T, E] (Chapter 8 and Chapter 12). The postfix ? operator unwraps an Ok or returns the Err from the enclosing function:

@mode(web.compiled)

func parsePort(text: string): Result[int, string] {
    if (text == "") {
        return Err("empty port");
    }
    return Ok(text.to_int());
}

func address(host: string, port: string): Result[string, string] {
    let p = parsePort(port)?;
    return Ok(i"{$ host $}:{$ p $}");
}

func main(): void {
    for (port in ["8080", ""]) {
        match address("localhost", port) {
            Ok(a) => println(a),
            Err(e) => println(i"error: {$ e $}"),
        }
    }
}

Listing 5-15: ? on a Result

localhost:8080
error: empty port

? works only inside a function that returns Result. Using it on an Option, in a function that returns Option, is a compile error:

This code does not compile
@mode(web.compiled)

func firstWord(line: string): Option[string] {
    let parts = line.split(" ");
    if (parts[0] == "") {
        return None;
    }
    return Some(parts[0]);
}

func shout(line: string): Option[string] {
    let word = firstWord(line)?;
    return Some(word.to_uppercase());
}

func main(): void {
    println(repr(shout("hello world")));
}
main.lpweb:12:16: error[E_PROPAGATE_WRONG_RETURN]: E_PROPAGATE_WRONG_RETURN: the '?' operator can only be used inside a function returning Result (§1-5.7)

For an Option, match on it, or supply a default with ??:

@mode(web.compiled)

func firstWord(line: string): Option[string] {
    let parts = line.split(" ");
    if (parts[0] == "") {
        return None;
    }
    return Some(parts[0]);
}

func shout(line: string): Option[string] {
    return match firstWord(line) {
        Some(word) => Some(word.to_uppercase()),
        None => None,
    };
}

func main(): void {
    println(repr(shout("hello world")));
    println(repr(shout("")));
    println(firstWord("") ?? "(no words)");
}

Listing 5-16: Handling an Option with match and ??

Some("HELLO")
None
(no words)

repr prints a value in its diagnostic form, which shows Some(…) and None; println on an Option prints the inner value, or nothing for None.

attempt is the in-place counterpart of ? (Language Reference §1-5.10). Inside a try block, attempt expr unwraps an Ok, or jumps to the block's catch Err(e) arm with the error value, without leaving the function:

@mode(web.compiled)

func parseQty(text: string): Result[int, string] {
    if (text == "") {
        return Err("quantity is empty");
    }
    return Ok(text.to_int());
}

func main(): void {
    for (raw in ["3", ""]) {
        try {
            let qty = attempt parseQty(raw);
            println(i"qty = {$ qty $}");
        } catch Err(e) {
            println(i"bad input: {$ e $}");
        } finally {
            println("checked");
        }
    }
}

Listing 5-17: attempt with a catch Err arm

qty = 3
checked
bad input: quantity is empty
checked

A try block that uses attempt must have a catch Err arm; it can also have ordinary catch (T as e) clauses for exceptions.

A worked example: checking an order

This program combines a for loop with tuple destructuring, ? propagation, and a match with block arms:

@mode(web.compiled)

func parseQuantity(text: string): Result[int, string] {
    if (text == "") {
        return Err("missing quantity");
    }
    let n = text.to_int();
    if (n <= 0) {
        return Err(i"'{$ text $}' is not a positive quantity");
    }
    return Ok(n);
}

func checkLine(item: string, qtyText: string, stock: int): Result[int, string] {
    let qty = parseQuantity(qtyText)?;
    if (qty > stock) {
        return Err(i"only {$ stock $} left");
    }
    return Ok(qty);
}

func main(): void {
    let lines = [("pens", "3"), ("paper", ""), ("ink", "12"), ("tape", "x")];
    var units = 0;
    var rejected = 0;
    for ((item, qty) in lines) {
        match checkLine(item, qty, 10) {
            Ok(n) => {
                units = units + n;
                println(i"ok      {$ item $} x{$ n $}");
            },
            Err(reason) => {
                rejected = rejected + 1;
                println(i"reject  {$ item $}: {$ reason $}");
            },
        }
    }
    println(i"{$ units $} units accepted, {$ rejected $} lines rejected");
}

Listing 5-18: Validating order lines

ok      pens x3
reject  paper: missing quantity
reject  ink: only 10 left
reject  tape: 'x' is not a positive quantity
3 units accepted, 3 lines rejected

"x".to_int() returns 0 in 1.25.1 rather than reporting an error, which is why parseQuantity checks for a positive result.

Summary

  • Conditions go in parentheses and must be bool; chains use else if. 1.25.1 catches missing parentheses at compile time but a non-bool condition only at run time.
  • if (c) { a } else { b } is an expression. There is no ternary operator.
  • while, do { … } while (c);, for (x in xs), for (i in a..b) (exclusive end, loop headers only) and the counted for (var i = 0; …; …). The loop variable is immutable unless declared var.
  • Labels let break and continue target an outer loop.
  • match arms are pattern => expr, separated by commas, with literals, bindings, guards, _, tuple patterns and type patterns. There are no OR-patterns. Enum matches are checked for exhaustiveness at compile time; other matches fall through to MatchError at run time without a _ arm.
  • try / catch (T as e) / finally handle exceptions. defer is specified but does not run in 1.25.1, so use finally. with drives any Scoped[T] value.
  • ? propagates the Err of a Result from a function that returns Result; attempt sends it to the enclosing try's catch Err arm. Handle an Option with match or ??.