Error Handling
Floe replaces exceptions with Result<T, E> and replaces null checks with Option<T>. Every error path is visible in the type system.
Result
Section titled “Result”fn divide(a: number, b: number) -> Result<number, string> { match b { 0 -> Err("division by zero"), _ -> Ok(a / b), }}You must handle the result:
match divide(10, 3) { Ok(value) -> Console.log(value), Err(msg) -> Console.error(msg),}Ignoring a Result is a compile error:
// Error: Result must be handleddivide(10, 3)The ? Operator
Section titled “The ? Operator”Propagate errors early instead of nesting matches:
fn processOrder(id: string) -> Result<Receipt, Error> { const order = fetchOrder(id)? // returns Err early if it fails const payment = chargeCard(order)? // same here Ok(Receipt(order, payment))}The ? operator:
- On
Ok(value): unwraps tovalue - On
Err(e): returnsErr(e)from the enclosing function
Using ? outside a function that returns Result is a compile error.
The collect Block
Section titled “The collect Block”Sometimes you want to validate multiple things and collect all errors, not just the first one. The collect block changes ? from short-circuiting to accumulating:
fn validateForm(input: FormInput) -> Result<ValidForm, Array<ValidationError>> { collect { const name = input.name |> validateName? const email = input.email |> validateEmail? const age = input.age |> validateAge?
ValidForm(name, email, age) }}Inside collect {}:
- Each
?that hitsErrrecords the error and continues - If any failed, the block returns
Err(Array<E>)with all collected errors - If all succeeded, returns
Ok(last_expression)
The return type of a collect block is always Result<T, Array<E>>.
This is useful for form validation, batch processing, and anywhere you want to report all errors at once instead of stopping at the first one.
Option
Section titled “Option”fn findUser(id: string) -> Option<User> { match users |> find(.id == id) { Some(user) -> Some(user), None -> None, }}Handle with match:
match findUser("123") { Some(user) -> greet(user.name), None -> greet("stranger"),}npm Interop
Section titled “npm Interop”When importing from npm packages, Floe automatically wraps nullable types:
import { getElementById } from "some-dom-lib"// .d.ts says: getElementById(id: string): Element | null// Floe sees: getElementById(id: string): Option<Element>The boundary wrapping also converts:
T | undefinedtoOption<T>anytounknown
This means npm libraries work transparently with Floe’s type system.
todo and unreachable
Section titled “todo and unreachable”Floe provides two built-in expressions for common development patterns:
todo - Not Yet Implemented
Section titled “todo - Not Yet Implemented”Use todo as a placeholder in unfinished code. It type-checks as never, so it satisfies any return type. The compiler emits a warning to remind you to replace it.
fn processPayment(order: Order) -> Result<Receipt, Error> { todo // warning: placeholder that will panic at runtime}At runtime, todo throws Error("not implemented").
unreachable - Should Never Happen
Section titled “unreachable - Should Never Happen”Use unreachable to assert that a code path should never execute. Like todo, it has type never, but unlike todo, it does not emit a warning.
fn direction(key: string) -> string { match key { "w" -> "up", "s" -> "down", "a" -> "left", "d" -> "right", _ -> unreachable, }}At runtime, unreachable throws Error("unreachable").
When to Use Which
Section titled “When to Use Which”todo= “I haven’t written this yet” (development aid)unreachable= “This should never happen” (safety assertion)
Runtime Type Validation with parse<T>
Section titled “Runtime Type Validation with parse<T>”The parse<T> built-in validates unknown data against a type at runtime. The compiler generates the validation code - no runtime library needed.
// Validate JSON data against a typeconst user = json |> parse<User>?
// With inline record typesconst point = data |> parse<{ x: number, y: number }>?
// Validate arraysconst items = raw |> parse<Array<Product>>?parse<T> returns Result<T, Error>. Use ? to unwrap or match to handle errors:
match data |> parse<User> { Ok(user) -> Console.log(user.name), Err(e) -> Console.error(e.message),}Supported types: string, number, boolean, record types, Array<T>, Option<T>, and named types.
Comparison with TypeScript
Section titled “Comparison with TypeScript”| TypeScript | Floe |
|---|---|
T | null | Option<T> |
try/catch | Result<T, E> |
?. optional chain | match on Option |
! non-null assertion | Not available (handle the case) |
throw new Error() | Err(...) |