Errors
Errors are values, carried by the standard Result<T, E> type. The error
slot is a regular type, so it can be a union written inline:
File.read = (Path) -> Result<Bytes, IoError | NotFound | PermissionDenied> {
...
}
This is more ergonomic than Rust's approach, where each call site typically needs a dedicated error enum.
The ? Operator
The postfix ? operator propagates failure. It works on both
Result<T, E> and Option<T>:
- On
Result<T, E>: short-circuits with the error, otherwise unwraps toT. - On
Option<T>: short-circuits withNone, otherwise unwraps toT.
main = (Stdout) -> Result<Noop, Noop> {
Ok(42)?.print(Stdout)
match Some(7) {
None => "absent".print(Stdout),
Some(_) => "present".print(Stdout),
}
Ok(Noop)
}
Ok(42)? evaluates to 42 (because the Result is Ok); if it were
Err(_), the function would return early with that error.
Option vs Result
Option<T> and Result<T, Empty> are structurally similar but kept
distinct:
Nonemeans absent.Err(_)means failed.
The semantic difference is worth the duplication. Use Option when a
value can legitimately be missing; use Result when an operation can
legitimately fail.
Chaining
Because ? is postfix, error-propagating pipelines read top-down,
left-to-right:
File.readConfig = (Path) -> Result<Config, IoError | ParseError> {
File.read(Path)?
.parse()?
.validate()
}
Each ? unwraps the success case and lets the chain continue; the first
failure short-circuits the whole function.
Validated Construction
The same ? shows up at the construction site for types whose
constructor is fallible.
A type with a Type.Self declaration that returns Result<Self, E>
forces callers to handle the failure mode:
HttpClient.get(Url("https://example.com")?)?.print(Stdout)
Both ?s here are doing the same job: unwrapping a Result at the
point of use. The first handles Url parsing failure (InvalidUrl);
the second handles HttpClient.get failure (HttpError). The
function's return type then carries the union:
Result<Noop, HttpError | InvalidUrl>.
Error Naming
Errors are types like any other, and they should be named semantically
— by what failed, not by who emitted them. InvalidUrl, MalformedJson,
FileNotFound, PermissionDenied carry information; UrlError,
JsonError, FsError don't.
The exception is opaque wrappers around foreign error types: when
binding to a Rust crate whose error is a large enum with many variants,
it's pragmatic to keep the wrapper opaque (e.g., HttpError for the
entirety of reqwest::Error) until the underlying error space gets
decomposed into proper Oneway variants.