Errors: Result & ?
E# treats errors as values, not control flow. A fallible function returns Result<T, E>, and
the ? operator threads the failure path for you. Exceptions (try/catch) exist, but they’re
for the boundary with the BCL — not how E# code talks to itself.
Result<T, E>
Section titled “Result<T, E>”Result<T, E> is either an ok(value) or an error(value). The error type is yours to design —
very often a choice enumerating exactly what can go wrong.
choice DbError { notFound connectionFailed(message: string) timeout}
func findUser(id: Guid, db: DbContext) -> Result<User, DbError> { let user = db.Users.Find(id) if user == nil { return error(.notFound) } return ok(user)}ok(...) and error(...) construct the two sides; .dotCase shorthand works inside error(...)
when the type is known from context. You can inspect a result with .IsOk / .IsError,
.Value / .Error, or the fluent combinators (.Map, .Bind, .UnwrapOr, .Match, …).
The ? operator
Section titled “The ? operator”? unwraps a Result: if it’s ok, you get the value; if it’s error, the enclosing function
returns that error immediately. It collapses the usual check-and-return boilerplate into one
character.
func getProfile(id: Guid, db: DbContext) -> Result<Profile, DbError> { let user = findUser(id, db)? // returns the DbError if findUser failed let prefs = findPreferences(user.id, db)? return ok(Profile { user: user, prefs: prefs })}Because ? returns the error from the enclosing function, that function’s error type has to be
compatible — which is why a module tends to share one error choice across its fallible
functions.
? is a real expression operator — it works anywhere an expression can appear, not just in a
let. Use it in a return, as a call argument, or as a bare statement (where the unwrapped value
is discarded but the error still propagates):
return ok(parse(raw)? + 1) // in a returnwrite(parse(raw)?) // as an argumentvalidate(input)? // bare statement — propagate on error, else continue(One spelling to know: result?.member currently reads as the null-conditional ?. operator, so
to unwrap-then-chain you parenthesize — (result?).member — or use a let.)
let-else guards
Section titled “let-else guards”When a value might be absent, a let ... else guard binds it or diverges:
let user = db.find(id) else { return error(.notFound)}// user is non-nil from here onThe else block must leave the scope (a return, break, etc.).
Optionals vs. Result
Section titled “Optionals vs. Result”These are different tools:
T?— optional presence. “There might not be a value.” Value types becomeNullable<T>; reference types stay as-is.Result<T, E>— a fallible operation. “This can fail, and here’s why it failed.”
Use T? for “maybe there’s a user”; use Result for “the lookup can fail with notFound /
timeout / connectionFailed.”
try / catch at the boundary
Section titled “try / catch at the boundary”The BCL throws. When you call into it, catch at that seam and translate into your own model —
then go back to Result internally.
func parsePort(s: string) -> Result<int, string> { try { return ok(int.Parse(s)) } catch (FormatException e) { return error("bad port: {e.Message}") }}There’s no finally keyword — use defer inside the try for cleanup.
A bare catch { } swallows anything when you don’t need the exception value. The rule of thumb:
catch exceptions at the edge, pass values everywhere else.