Skip to content

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> 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, …).

Browse Result examples →

? 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 return
write(parse(raw)?) // as an argument
validate(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.)

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 on

The else block must leave the scope (a return, break, etc.).

These are different tools:

  • T?optional presence. “There might not be a value.” Value types become Nullable<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.”

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.