When a Call Can Fail
Hale handles recoverable, value-level errors through fallible(E) function signatures and mandatory or clauses at every call site. Unlike exceptions, errors never propagate invisibly across stack frames; unlike Rust’s Result<T, E> or Go’s (T, error), a fallible return is not a wrapper value that can be stored or ignored—it must be resolved immediately where the call is made.
type ParseError { kind: String; input: String; }
fn parse_count(s: String) -> Int fallible(ParseError) { if !std::str::can_parse_int(s) { fail ParseError { kind: "not_int", input: s }; } return std::str::parse_int(s) or 0;}
fn main() { let valid = parse_count("42") or 0; let invalid = parse_count("banana") or -1; println(f"valid={valid} invalid={invalid}");}Declaring fallible(E) and Raising Errors
Section titled “Declaring fallible(E) and Raising Errors”Append fallible(E) after a function’s return type (or directly after the parameter list if the function returns ()) to declare that it may fail with error payload E:
fn write_report(path: String, body: String) fallible(IoError) { std::io::fs::write_file(path, body) or raise;}Inside a fallible(E) function, fail <expr>; exits the function immediately along its error channel with <expr> of type E.
Addressing Fallible Calls with or
Section titled “Addressing Fallible Calls with or”Every call to a fallible(E) function must be followed immediately by an or clause. Calling a fallible function bare (parse_count(s);), passing it directly as an argument, or matching on it (match parse_count(s) { ... }) is a compile error in hale check, hale verify, and hale build.
or Form |
Syntax Example | Behavior |
|---|---|---|
| Fallback value | parse_count(s) or 0 |
Substitutes a default value of the success type (err is in scope inside the fallback expression). |
| Fallback block | parse_count(s) or { log(err.kind); return; } |
Runs a block with err: E in scope. In value position, the block must evaluate to T or diverge (return, break, continue, fail, violate). |
Propagate (raise) |
parse_count(s) or raise |
Returns E from the enclosing fallible(E) function. In an infallible function (such as main), halts the process with Hale panic: unhandled E escaping main locus. |
| Handler function | parse_count(s) or handle_err(err) |
Calls a helper fn(E) -> T with err to compute a substitute value (or trigger structural shutdown). |
Translate (fail) |
parse_count(s) or fail AppErr { msg: err.kind } |
Fails the enclosing fallible(AppErr) function with a translated error payload. |
Ignore (discard) |
cache.put(k, v) or discard; |
Drops the error. Valid anywhere for () functions, or at statement position when discarding both value and error. |
Examples of Each or Disposition
Section titled “Examples of Each or Disposition”type AppErr { msg: String; }
fn default_port(e: ParseError) -> Int { eprintln("bad port (", e.input, "), defaulting to 8080"); return 8080;}
fn read_port(raw: String) -> Int fallible(AppErr) { return std::str::parse_int(raw) or fail AppErr { msg: err.kind };}
fn main() { let p1 = std::str::parse_int("9000") or 8080; let p2 = std::str::parse_int("bad") or default_port(err); let p3 = read_port("3000") or { eprintln("fatal: ", err.msg); return; }; std::io::fs::mkdir("tmp_cache") or discard; println(p1, " ", p2, " ", p3);}Statement vs. Value Position Rules
Section titled “Statement vs. Value Position Rules”- Statement-position
orblocks: When a fallible call stands as a complete statement (parse_count(s) or { log(err.kind); };), its success value is discarded, so theorblock does not need to produce a value of the success type. - No binding
()calls: A fallible function whose success type is()cannot be bound to aletvariable (let ok = std::io::fs::write_file(p, s) or { };is rejected); call it as a statement instead. - No
or discardin value bindings: Writinglet n = parse_count(s) or discard;is a compile error becausenwould have no value on failure; useor <fallback>,or raise, or callparse_count(s) or discard;as a statement.
Chaining and Fallible Recovery Handlers
Section titled “Chaining and Fallible Recovery Handlers”or clauses associate right-to-left, allowing you to chain fallback attempts before a final default or raise:
let id = parse_count(primary) or parse_count(fallback) or 0;When a recovery expression in an or clause can itself fail:
- User-defined fallible functions implicitly propagate their error to the enclosing
falliblefunction (db_read(k) or self.rebuild(k)). - Stdlib functions, generic functions, interface methods, and
@formmethods used inside anorarm require an explicit innerordisposition:
fn load(primary: String, backup: String) -> String fallible(IoError) { return std::io::fs::read_file(primary) or (std::io::fs::read_file(backup) or raise);}Fallible Methods in Interfaces
Section titled “Fallible Methods in Interfaces”Interface method signatures may declare fallible(E):
type StoreError { kind: String; }
interface Store { fn put(key: String) -> Int fallible(StoreError);}
locus StrictStore { fn put(key: String) -> Int fallible(StoreError) { if key == "" { fail StoreError { kind: "empty key" }; } return len(key); }}
fn store_or_zero(s: Store, key: String) -> Int { return s.put(key) or 0;}A locus whose put method is infallible still satisfies Store (since it never exercises the error path). A locus whose put fails with a different error type, or declares fallible when the interface method is infallible, fails typechecking.
Two Failure Channels: Why Lifecycle Hooks Cannot Be fallible(E)
Section titled “Two Failure Channels: Why Lifecycle Hooks Cannot Be fallible(E)”Hale separates failures into two orthogonal channels:
- Value channel (
fallible(E)+or): Caller-addressed operational errors on free functions and user-declaredfnmethods on a locus. - Structural channel (
↑— closure tests,violate, andon_failure): Component-level invariant violations and supervision policies, covered in When Things Fail.
Next: Your First Program.