Errors: Result & ?
E# treats expected failure as values: a fallible function returns Result<T, E>, and the
? operator threads the failure path for you. Exceptions (try / catch / throw) are the other
half — the CLR’s path for exceptional conditions, and a real part of E#. The two aren’t separate
worlds: the BCL throws, so you catch at that seam and translate into a Result, and even Result’s
own escape hatches throw. Expected failure → a Result; exceptional → an exception; convert between
them where they meet.
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 union enumerating exactly what can go wrong.
union 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 union 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.”
Exceptions: try / catch
Section titled “Exceptions: try / catch”Exceptions are E#‘s other failure channel — for the exceptional, where Result is for the
expected. The BCL throws, so the most common move is to 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 (e: FormatException) { 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 fit, not a
prohibition: model expected failure as a Result, let exceptional conditions throw, and convert
between them where they meet — most often at the BCL edge.