Skip to content

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

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 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 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.”

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.