Skip to content

Errors

E# expresses failure two complementary ways. Result<T, E> and the ? operator are the primary, value-based surface — expected, recoverable failure modeled as a value you pass, inspect, and propagate, and how E# code idiomatically signals fallibility to itself. Exceptions are the CLR’s mechanism for exceptional conditions; try / catch / throw handle them, and they stay a real concern — the BCL throws, and even a Result’s escape hatches (Unwrap, .Value on the wrong variant) throw. The two are not separate worlds but two ends of one error model, split by whether a failure is expected (return a Result) or exceptional (let it throw), and they convert at the boundary: catch a thrown exception where it arises, translate it into a Result, and continue in value-based flow.

This page remains the complete error-model reference. The focused subpages give the full normative shape of the two value-flow mechanisms:

  • Result values and operations — representation, construction, accessors, match case views, combinators, and CLR interop.
  • Propagation with ? — postfix grammar, short-circuit behavior, expression positions, async completion, and the boundaries with ?. and ??.

Result<TValue, TError> is a builtin. The compiler lowers every surface below against one concrete type — the value struct Esharp.Stdlib.Result`2, authored in E# in the standard library and resolved by metadata name — so the language surface and direct C# interop are backed by the same type, with no generated facade.

Its representation is a value struct with exactly three fields:

pub struct Result<TValue, TError> {
pub IsOk: bool
pub Value: TValue
pub Error: TError
}

A Result carries its discriminant and both payload slots inline, is copied on assignment, has no identity, and costs no allocation on the hot path — every ok, error, and ? works against the struct directly. The fields are public because the compiler’s accessor intrinsics read them across the assembly boundary; the variant invariantValue is meaningful iff IsOk, Error is meaningful iff !IsOk — is upheld by construction, because ok / error / the combinators are the only ways a Result is built.

A TError is user-chosen and commonly a union enumerating exactly what can go wrong.

The constructors ok(value) and error(value) are expressions and the idiomatic way to build a Result. They are compiler intrinsics: each lowers to an inline initobj + field stores on the pinned struct (zeroing the unused variant slot), so construction never calls through a factory and never allocates. Inside error(...) the .case dot-shorthand resolves against the known error type.

let a = ok(42) // ok side
let e = error(.notFound) // error side; .notFound resolves against the error union

A static factory class mirrors the constructors for explicit and interop use. The value type Result`2 and the static class Result (arity 0) coexist under the one name by arity keying; the factory is callable identically from E# and C#:

let b = Result.Ok<int, string>(42) // explicit type arguments
let c = Result.Error<int, string>("nope")

The error side therefore has three distinct, deliberate spellings, each in its own position:

SpellingPositionBuilds / names
error(e)expression — constructionthe intrinsic constructor
Result.Error<T, E>(e)expression — explicit factorythe static factory, for interop and explicit type arguments
.err(e)a match armthe error case view

(The success side parallels it: ok(v), Result.Ok<T, E>(v), and the .ok(v) match arm.)

Four accessor intrinsics read a Result directly:

AccessorTypeBehavior
.IsOkboolthe discriminant field
.IsErrorboolthe negation of IsOk — synthesized, not a stored field
.ValueTValuethe success payload; throws InvalidOperationException on an error
.ErrorTErrorthe error payload; throws InvalidOperationException on a success

The accessor intrinsic guards each field load, so reading the wrong variant’s payload is the documented failure — the variant invariant is enforced at the read, not merely trusted. .IsError exists for readable branch tests even though the underlying struct stores only IsOk:

func describe(r: Result<int, string>) -> string {
if r.IsError { return "failed: {r.Error}" }
return "ok: {r.Value}"
}

Ten combinators are methods — each has a Result receiver, so it reads as r.Method(...) (Functions → methods). They compose in fully-typed contexts: type arguments flow through the receiver and through lambda arguments (Generics → type-argument inference), so the chain below stays typed end to end with every lambda parameter and result inferred.

MethodSignature (receiver r: Result<T, E>)Result
Map(f: Func<T, U>) -> Result<U, E>transform the ok value; error passes through
MapErr(f: Func<E, F>) -> Result<T, F>transform the error; ok passes through
Bind(f: Func<T, Result<U, E>>) -> Result<U, E>chain a fallible step onto the ok value (monadic bind)
Match(ok: Func<T, O>, err: Func<E, O>) -> Ofold both variants into one type
Inspect(onOk: Action<T>) -> Result<T, E>side-effect on ok; returns r unchanged
InspectErr(onErr: Action<E>) -> Result<T, E>side-effect on error; returns r unchanged
UnwrapOr(fallback: T) -> Tthe ok value, or a constant fallback
UnwrapOrElse(fallback: Func<E, T>) -> Tthe ok value, or a fallback computed from the error
Unwrap() -> Tthe ok value; throws on error
UnwrapErr() -> Ethe error value; throws on success
func go() -> int = parse(10).Map((x) => x + 1).Bind((x) => dbl(x)).Match((v) => v - 1, (e) => -1)

.Match (the combinator) and match (the statement, below) are two routes to the same fold; .Match is the expression-position form that returns directly into a chain. Unwrap / UnwrapErr are the throwing escapes — the conscious hot-path exception, parallel to .Value / .Error.

Each combinator is a value-receiver method on the Result struct (the receiver block (r: Result<…>)), so it reads as r.Method(...) — a snapshot copy of the value, never mutating the original. The receiver threads through the generic parameters and the method adds its own, and the success/error variant is read through the .IsOk discriminant:

pub func (r: Result<TValue, TError>) Map<TValue, TError, TNew>(f: Func<TValue, TNew>) -> Result<TNew, TError> {
if r.IsOk {
return ok(f(r.Value))
}
return error(r.Error)
}
pub func (r: Result<TValue, TError>) UnwrapOr<TValue, TError>(fallback: TValue) -> TValue {
if r.IsOk {
return r.Value
}
return fallback
}
pub func (r: Result<TValue, TError>) Unwrap<TValue, TError>() -> TValue {
if not r.IsOk {
throw InvalidOperationException("called Unwrap on an error Result")
}
return r.Value
}

A static Result factory class pairs with the Result<TValue, TError> value type by arity keyingResult`2 (the struct) and Result (arity-0 static class) coexist under the one name — and mirrors the ok / error intrinsics for explicit and C# interop use:

pub static Result {
pub func Ok<TValue, TError>(value: TValue) -> Result<TValue, TError> = ok(value)
pub func Error<TValue, TError>(err: TError) -> Result<TValue, TError> = error(err)
}

A Result is destructured with match over two case views, .ok and .err, binding the respective payload:

func go() -> int {
let a = parse(10)?
let b = step(a)?
match step(b) {
.ok(v) { return v + a + b }
.err(e) { return -1 }
}
return -2
}

The two cases are exhaustive, so a match covering both .ok and .err needs no default and satisfies definite return. match also works as an expression (every arm one common type) and composes under await, so an awaited Result is matched in place.

expr? evaluates expr to a Result and forks on the variant:

  • ok → the operator’s value is the unwrapped ok payload, left in place for the surrounding expression to use;
  • error → the enclosing function returns immediately. The compiler builds a fresh Result of the enclosing function’s return type and moves the inner result’s error value into it — the error type carries through unchanged, the success type re-targets to whatever the caller returns.

? is a real expression operator, not a statement form: it composes anywhere an expression appears, and each of these positions is distinct:

let user = findUser(id)? // a let initializer
return ok(parse(raw)? + 1) // inside a return, feeding ok(...)
write(parse(raw)?) // a call argument
validate(input)? // a bare statement — continue on ok, propagate on error
let n = step(parse(x)?)? // nested — the inner ? feeds a call whose result is ?'d again

In a chain of statements each carrying a ?, evaluation stops at the first error and that error is what propagates; later statements never run.

Each ? is its own ok/error fork, and a single evaluation step resolves at most one. A compound expression that would propagate two results is written as sequential lets — one ? per step, which is also where the names that document the flow live:

let a = f()?
let b = g()?
return ok(a + b)

This is the canonical shape for fanning several fallible calls into one expression, and it reads more clearly than nesting the propagations.

  • expr?.member is the null-conditional operator. The lexer fuses ?. into one token (Lexical → operators), so result?.field is a null-conditional access, never “unwrap result, then .field”. To unwrap-then-access, parenthesise — (result?).field — or bind the unwrapped value with a let. A fallible fluent chain (machine.insert(25)?.insert(10)?) is therefore written as sequential lets, one unwrap per step.
  • Ternary versus try-unwrap. A ? is read as the ternary operator when the following token can begin an expression, and as postfix try-unwrap otherwise (Expressions).

Because the propagated error value is reused as-is, the enclosing function’s error type is the same as the ?’d result’s error type. This is the practical reason a module standardizes on one error union: every ? in a call graph carries the same E, and only the success type changes from function to function.

? composes with await. When a ? propagates inside an asynchronous function, the early return is routed through the async builder’s completion path (SetResult) rather than a raw ret, so an error short-circuit out of a state machine is well-formed. Both the ok and error paths of ? are exercised under async.

ToolMeaning
T?optional presence — “there might not be a value”
Result<T, E>a fallible operation — “this can fail, and here is why”

Use T? for “maybe there is a user”; use Result for “the lookup can fail with notFound / timeout / connectionFailed.” See Type system → nullability.

A let … else guard binds a value or diverges; the else block shall leave the scope. The full grammar and rules are in Statements → let-else.

let user = db.find(id) else { return error(.notFound) }
// user is non-nil from here on

Exceptions are the CLR’s mechanism for exceptional conditions, and try / catch / throw are how E# handles them — the complement to the value-based Result flow above, not a thing apart from it. Because the BCL throws, the most common use is to catch at that seam and translate into a Result, returning to value-based flow:

func parsePort(s: string) -> Result<int, string> {
try {
return ok(int.Parse(s))
} catch (e: FormatException) {
return error("bad port: {e.Message}")
}
}

There is no finally — use defer inside the try. throw e raises an exception; throw with no operand rethrows and is valid only inside a catch. The three catch shapes and the grammar are in Statements → try / catch / throw. The guideline is fit, not prohibition: model expected failure as a Result, let exceptional conditions throw, and convert between the two where they meet.