Result and Option

Sec uses explicit carrier types for two common situations:

Option[T]
Result[T, E]

Option[T] represents the presence or absence of an ordinary value. Result[T, E] represents successful completion or a typed error.

Core rule: Absence and failure are different concepts. None is not an error, and Err(...) is not absence.

Carrier Types

Option[T] and Result[T, E] are predeclared generic types available without a standard-library import.

Their logical states are:

Type State 1 State 2
Option[T] Some(T) None
Result[T, E] Ok(T) Err(E)

The active state determines which payload, if any, is currently owned by the carrier.

Option[T]

Option[T] represents an optional value of type T.

fn FindDevice(name: string) Option[Device] {
    ...
}

The result is either:

Some(device)
None

Use Option when absence is a normal part of the domain rather than a failure that needs an error value.

Examples include:

  • searching for an item that may not exist;
  • looking up an optional configuration value;
  • representing an optional safe reference;
  • an iterator reaching the end of its sequence.

Some and None

Some(value) constructs the present state.

return Some(device)

None constructs the absent state.

return None

Some(value) requires value to be assignable to the declared payload type T.

None carries no T payload.

Option Is Not an Error Channel

None represents absence only.

It does not contain:

  • an error code;
  • an exception;
  • a hidden reason string;
  • a default error value.

When callers need to know why an operation failed, use an appropriate Result[T, E] instead.

Optional Safe References

Safe references are non-null, so optional reference semantics use Option:

Option[ref Device]
Option[ref mut Device]

This keeps absence explicit without adding a null state to ref T or ref mut T.

When the option contains a reference, normal reference lifetime and borrowing rules remain active.

Returning an Option

Construct the state explicitly when returning a payload or absence:

fn FindDevice(name: string) Option[Device] {
    if HasDevice(name) {
        return Some(LoadDevice(name))
    }

    return None
}

If an expression already has the exact compatible Option[T] type, the complete carrier may be returned directly:

fn Find() Option[Device] {
    ...
}

fn FindPreferred() Option[Device] {
    return Find()
}

Direct return preserves whichever state is active. The carrier is not unwrapped and reconstructed.

Result[T, E]

Result is a predeclared nominal generic type with exactly two type arguments:

Result[ValueType, ErrorType]

For:

Result[T, E]
  • T is the success type;
  • E is the error-channel type;
  • the active value is either Ok(T) or Err(E).

Examples:

Result[Data, IOError]
Result[Data, error]
Result[void, ParseError]

Invalid examples:

Result[int]
Result[int, IOError, string]
Result[int, string]

The error type must satisfy the Sec error-type rules.

The Error Channel

E must be either:

  • the compiler-known root type error;
  • a concrete type declared as a Sec error type.

Ordinary types such as string, int or an unrelated struct cannot be used as the error channel merely because they could describe a failure.

Concrete error declarations are covered in the next chapter.

Precise and Open Error Channels

Prefer a concrete error type when the API can describe its failure domain precisely:

fn Read() Result[Data, IOError] {
    ...
}

Use error when the API intentionally accepts or forwards heterogeneous Sec error values:

fn RunPipeline() Result[Data, error] {
    ...
}

Result[T, IOError] has a precise concrete error channel. Result[T, error] has an open error channel.

Widening a concrete error into error preserves its concrete identity, active variant, payload and ownership state.

Ok and Err

Ok and Err construct the two Result states:

return Ok(value)
return Err(IOError.ReadError)

Ok(value) requires value to be assignable to the declared success type.

Err(errorValue) requires the error value to be assignable to the declared error channel.

A concrete error may therefore be returned through Result[T, error] using normal error widening.

Result[void, E]

When success carries no value, use void as the success type:

fn Save() Result[void, IOError] {
    ...
    return Ok()
}

Ok() is valid only when the success type is void.

This is invalid for a value-bearing result:

fn Read() Result[Data, IOError] {
    return Ok()    // invalid
}

Returning a Complete Result

An expression that already has the compatible complete Result[T, E] type may be returned directly:

fn ReadFromDevice() Result[Data, IOError] {
    ...
}

fn Read() Result[Data, IOError] {
    return ReadFromDevice()
}

The active Ok or Err state is forwarded unchanged.

Direct carrier return does not unwrap and then reconstruct the value.

Plain Values Are Not Implicitly Wrapped

Returning a complete carrier directly does not create an implicit payload conversion.

This remains invalid:

fn Find() Option[Device] {
    let device := LoadDevice()
    return device    // invalid
}

Write:

return Some(device)

Likewise, this is invalid:

fn Read() Result[Data, IOError] {
    let data := LoadData()
    return data    // invalid
}

Write:

return Ok(data)

The carrier state remains explicit whenever the expression being returned is only the payload.

Matching Option

match is the general explicit way to handle both states of an Option[T].

match FindDevice(name) {
    Some(device) => Use(device)
    None => HandleMissing()
}

A complete Option[T] match has exactly two carrier states: Some(...) and None.

The binding inside Some(...) follows ordinary match ownership, borrowing and guard rules.

Matching Result

match can make success and failure equally explicit:

match Read() {
    Ok(data) => Process(data)
    Err(errorValue) => Handle(errorValue)
}

The Ok branch receives the success payload. The Err branch receives the declared error payload.

For a precise error type, the error payload may then be matched more specifically according to that error type's enum or union structure.

For Result[T, error], exhaustive handling of the open error channel requires an appropriate fallback when concrete error cases do not cover the complete domain.

Carrier Patterns Must Match the Carrier Type

Result patterns and Option patterns are not interchangeable.

This is invalid:

match FindDevice(name) {
    Ok(device) => Use(device)    // invalid: subject is Option[Device]
    Err(_) => {}
}

Use:

match FindDevice(name) {
    Some(device) => Use(device)
    None => {}
}

Likewise, this is invalid for a Result:

match Read() {
    Some(data) => Process(data)    // invalid
    None => {}
}

The compiler and LSP should explain the carrier family and suggest the correct pattern names rather than reporting only a generic mismatch.

Result Projections

Result[T, E] has four compiler-known projections for intentionally keeping one side and forgetting the other.

Member Result type Receiver behavior
Ok() Option[T] Consumes the Result
Err() Option[E] Consumes the Result
OkRef Option[ref T] Shared non-consuming property
ErrRef Option[ref E] Shared non-consuming property

These members do not provide an unchecked unwrap operation. Every projection still returns an Option.

result.Ok()

The consuming success projection keeps the success payload when the Result is Ok:

Ok(value).Ok()  -> Some(value)
Err(error).Ok() -> None

The complete Result receiver is consumed.

When the retained payload is move-only, it is moved into the returned Option[T].

The operation does not clone the payload.

result.Err()

The consuming error projection keeps the error payload when the Result is Err:

Err(error).Err() -> Some(error)
Ok(value).Err()  -> None

As with Ok(), the complete Result receiver is consumed.

If the active non-retained payload requires non-discardable handling, the projection is invalid unless control-flow analysis proves that such a state cannot occur.

OkRef and ErrRef

Borrowed projections inspect one side without consuming the Result.

let result := Read()

if result.ErrRef is Some(errorValue) {
    Inspect(errorValue)
}

Use(result)

Their semantics are:

Ok(value).OkRef   -> Some(ref value)
Err(error).OkRef  -> None

Err(error).ErrRef -> Some(ref error)
Ok(value).ErrRef  -> None

These properties:

  • do not consume the Result;
  • do not copy or clone a payload;
  • create a normal shared borrow only when the requested payload is active;
  • cannot outlive the Result or the active payload.

Sec 0.1 does not define mutable borrowed Result projections.

Choosing Between Consuming and Borrowed Projections

Use Ok() or Err() when the complete Result is no longer needed:

if Clear().Err() is Some(errorValue) {
    Handle(errorValue)
}

Use OkRef, ErrRef or a borrowed match when the original Result must remain available:

let result := Read()

if result.ErrRef is Some(errorValue) {
    Inspect(errorValue)
}

Use(result)

After a consuming projection, later use of the original Result is invalid:

let result := Read()
let value := result.Ok()

Use(result)    // invalid

Result Is Always Must-Use

A Result-producing operation cannot be silently ignored.

This is invalid:

TryWriteLog()

Valid handling includes:

  • using try;
  • using match;
  • binding the complete Result;
  • returning the Result;
  • passing it to a valid consuming context;
  • explicitly discarding it when the complete carrier is discardable;
  • using .Ok() or .Err() to intentionally select one side.

Must-use prevents an error result from disappearing accidentally.

Explicitly Discarding a Result

When both success and failure are intentionally ignored, say so explicitly:

discard TryWriteLog()

This means the complete Result is intentionally discarded.

It is legal only when whichever payload may be active is itself legally discardable.

discard does not silently suppress cleanup obligations.

Carrier Payloads Follow Normal Ownership Rules

Option and Result do not create a separate ownership model.

The active payload may itself own data or be move-only.

let optional := Some(<-resource)
let result := Ok(<-buffer)

In a non-terminal construction, moving an existing reusable source into the carrier uses the normal explicit move syntax.

Once stored in the carrier, the carrier owns the active owned payload.

Copy and Move Behavior Is Derived from the Payloads

The carrier does not secretly clone move-only payloads.

If the active payload cannot be implicitly copied, an operation that would require such a copy is invalid.

Moving a carrier transfers its active payload according to ordinary move semantics.

Consuming projections similarly move the retained move-only payload into the returned Option rather than cloning it.

Terminal Return Forwards the Carrier and Active Payload

Returning a complete carrier uses ordinary terminal-return ownership forwarding:

fn Read() Result[Data, IOError] {
    let result := ReadFromDevice()
    return result
}

Likewise for Option:

fn Find() Option[Device] {
    let result := FindDevice()
    return result
}

No redundant return-site move marker is required.

The active payload is forwarded with the carrier rather than copied merely because it crosses the return boundary.

Borrowing Carrier Payloads

Borrowed match patterns may inspect an active payload without taking ownership:

let result := Read()

match result {
    Ok(ref data) => Inspect(data)
    Err(ref errorValue) => InspectError(errorValue)
}

The resulting references are ordinary safe borrows. They cannot outlive the carrier payload they reference.

OkRef and ErrRef provide compiler-known shorthand for the common shared-inspection case.

Destruction Follows the Active State

When a carrier is destroyed, only the currently active payload requires payload destruction.

The payload follows its ordinary deterministic destruction rules.

Consuming projections destroy the non-retained active payload exactly once when such destruction is required and legal.

There is no hidden garbage-collection or exception-based cleanup model for Option or Result.

Do Not Assume an Implicit Default Carrier State

The representation of Option[T] or Result[T, E] does not by itself define an implicit default value.

In particular, the compiler must not infer a default merely because one of these states exists:

None
Ok(default(T))
Err(default(E))

Defaultability requires an explicit canonical rule for the carrier.

Do not treat representation zero as sufficient proof that a valid carrier default exists.

A Short Preview of try

try can short-circuit both carrier families:

fn ReadData() Result[Data, IOError] {
    let data := try ReadFile()
    return Ok(data)
}
fn Resolve() Option[Device] {
    let device := try FindDevice()
    return Some(device)
}

For a Result:

Ok(value) -> success value
Err(error) -> propagated or locally handled failure

For an Option:

Some(value) -> success value
None        -> propagated or locally handled absence

The complete try syntax, local handlers, guards, failure propagation and fallible operations are covered in Errors and try.

No Implicit Conversion Between Absence and Error

Sec never invents a mapping between the two carrier families.

None -/> Err(...)
Err(...) -/> None

An Option-returning function cannot propagate a Result error merely because both operations can short-circuit.

A Result-returning function cannot silently reinterpret None as an error.

When an API needs such a conversion, the programmer states the mapping explicitly.

Option Is Not null

Sec does not use a null safe-reference state for ordinary optionality.

Use:

Option[ref Device]

rather than inventing:

null ref Device

The unsafe FFI/raw-pointer null sentinel is a different concept covered by the Unsafe and FFI chapters.

Each Generic Carrier Specialization Has Its Own Type

Different concrete carrier specializations are not interchangeable merely because their active state currently looks similar.

Option[Device]
Option[Socket]

Result[Data, IOError]
Result[Data, ParseError]

A plain payload does not implicitly convert into a carrier, and one incompatible carrier specialization does not silently become another.

Error widening to the root error is the specific exception defined by the Sec error model.

Carrier Representation Is Not Source Semantics

The source model guarantees the carrier states and their ownership behavior.

It does not require one universal physical representation.

A backend may use:

  • tagged storage;
  • niche representations;
  • direct branches that avoid materializing a temporary carrier;
  • other target-appropriate representations.

Such lowering is valid only when observable Sec semantics remain identical.

Representation choices must not change whether a payload is present, whether an error is active, ownership, destruction, borrow lifetime or must-use behavior.

Diagnostics

Diagnostics should identify the carrier family, active semantic requirement and ownership consequence rather than reducing every mistake to a generic type mismatch.

Wrong carrier pattern

`FindDevice()` returns `Option[Device]`

`Ok(...)` is a Result pattern and cannot match Option

help: use `Some(device)` and `None`

Implicit Option wrapping

cannot return `Device` from a function returning `Option[Device]`

help: return `Some(device)`
help: return `None` when the value is absent

Implicit Result wrapping

cannot return `Data` from a function returning `Result[Data, IOError]`

help: return `Ok(data)`
help: return `Err(errorValue)` on failure

Invalid Result error type

`string` cannot be used as the error channel of `Result[int, string]`

the error type must be `error` or a concrete Sec error type

Ignored Result

result of `TryWriteLog()` must be handled

help: use `try`
help: match the Result
help: bind or return the Result
help: explicitly discard it when both payload states are discardable

Use after consuming projection

`result` is unavailable after `result.Ok()`

`Ok()` consumes the complete Result receiver

help: use `OkRef` for shared non-consuming inspection
help: inspect the Result with a borrowed match pattern

Invalid cross-channel propagation

cannot propagate `None` through `Result[Device, LookupError]`

Option absence is not a Result error

help: map `None` to an explicit error value
help: handle the absence locally

Language-Server and Tooling Behavior

Tooling should expose the semantic structure of carriers directly.

Hover and analysis may show:

  • Option[T] payload type;
  • Result[T, E] success and error types;
  • whether a Result error channel is concrete or the open error root;
  • whether Ok() or Err() consumes the receiver;
  • the result type of a projection;
  • the borrow created by OkRef or ErrRef;
  • the source location that consumed a carrier before an invalid later use;
  • the correct Some/None or Ok/Err patterns;
  • the alternate state that a later try will propagate.

Completion should not suggest Result patterns for an Option subject or Option patterns for a Result subject.

Formatter behavior preserves canonical casing:

Option[T]
Some(value)
None

Result[T, E]
Ok(value)
Err(errorValue)

result.Ok()
result.Err()
result.OkRef
result.ErrRef

Best Practices

  • Use Option[T] when absence is an ordinary domain outcome rather than an error.
  • Use Result[T, E] when callers need an explicit typed failure channel.
  • Prefer a concrete error type over the root error when the API has a well-defined failure domain.
  • Use Result[T, error] when heterogeneous Sec errors intentionally cross one API boundary.
  • Construct payload states explicitly with Some, Ok and Err.
  • Return an already-compatible complete carrier directly instead of unpacking and rebuilding it.
  • Use match when both carrier states should be explicit in source.
  • Use Ok() or Err() only when consuming the complete Result is intended.
  • Use OkRef, ErrRef or borrowed matching when the Result must remain available.
  • Do not ignore a Result-producing operation accidentally.
  • Use discard only when both possible active payloads may legally be discarded.
  • Do not expect carrier construction or projection to clone move-only payloads.
  • Do not use Option as a disguised error channel when callers need failure information.
  • Do not use Result merely to represent ordinary optionality.
  • Do not convert between None and Err implicitly; state the mapping explicitly.
  • Use Option[ref T] rather than nullable safe references.

Summary

  • Option[T] and Result[T, E] are predeclared generic carrier types available without a standard-library import.
  • Option[T] represents ordinary presence or absence.
  • The two Option states are Some(T) and None.
  • None is not an error and carries no hidden error value.
  • Optional safe references use forms such as Option[ref T] and Option[ref mut T].
  • Safe references themselves remain non-null.
  • Some(value) explicitly constructs a present Option value.
  • None explicitly constructs the absent Option state.
  • A complete compatible Option[T] expression may be returned directly.
  • Direct Option return preserves the complete active carrier state.
  • A plain T is not implicitly wrapped in Some(T).
  • Result[T, E] has exactly two generic arguments.
  • T is the success type.
  • E is the declared error-channel type.
  • E must be error or a concrete Sec error type.
  • The two Result states are Ok(T) and Err(E).
  • Ok(value) explicitly constructs the success state.
  • Err(errorValue) explicitly constructs the failure state.
  • Result[void, E] represents successful completion without a success payload.
  • Ok() is the success constructor for Result[void, E].
  • Ok() is invalid for a value-bearing Result success type.
  • A concrete error may widen into a Result[T, error] error channel while preserving concrete error identity and payload.
  • Result[T, ConcreteError] is a precise concrete error channel.
  • Result[T, error] is an intentionally open error channel.
  • A complete compatible Result[T, E] expression may be returned directly.
  • Direct Result return forwards the complete active carrier rather than unwrapping and rebuilding it.
  • A plain success payload T is not implicitly converted to Result[T, E].
  • Option matching uses Some(...) and None.
  • Result matching uses Ok(...) and Err(...).
  • Option and Result pattern families are not interchangeable.
  • match is the general explicit construct for handling both carrier states.
  • Result has compiler-known consuming projections Ok() and Err().
  • result.Ok() returns Option[T].
  • result.Err() returns Option[E].
  • Ok(value).Ok() becomes Some(value).
  • Err(error).Ok() becomes None.
  • Err(error).Err() becomes Some(error).
  • Ok(value).Err() becomes None.
  • Ok() and Err() consume the complete owned Result receiver.
  • Consuming projections do not clone move-only payloads.
  • A retained move-only payload is moved into the returned Option.
  • The non-retained active payload is destroyed exactly once according to ordinary destruction rules.
  • A consuming projection is invalid when it could abandon a non-discardable active obligation.
  • Result has compiler-known non-consuming shared projections OkRef and ErrRef.
  • OkRef returns Option[ref T].
  • ErrRef returns Option[ref E].
  • Borrowed projections do not consume or clone the Result payload.
  • Borrowed projections create an ordinary shared borrow only when the selected payload is active.
  • A borrowed projection cannot outlive the Result payload it references.
  • Sec 0.1 defines no mutable borrowed Result projection.
  • Use consuming projections when the complete Result is no longer required.
  • Use borrowed projections or borrowed matching when the Result must remain usable afterward.
  • Result[T, E] is always must-use.
  • A bare standalone Result-producing call cannot silently disappear.
  • Result may be handled through try, match, binding, returning, a valid consuming context, explicit discard, or an intentional consuming projection.
  • Explicitly discarding a Result ignores both states intentionally and is legal only when the complete carrier is discardable.
  • Option and Result active payloads follow ordinary ownership, copy/move, borrowing, lifetime, discardability and destruction rules.
  • Non-terminal construction of an owned move-only payload uses ordinary explicit move syntax.
  • The carrier owns an active owned payload once construction commits.
  • Carrier operations never invent a hidden clone for move-only payloads.
  • Terminal return forwards the complete carrier and active payload without a redundant return-site move marker.
  • Borrowed patterns may inspect active payloads without transferring ownership.
  • Carrier destruction destroys only the active payload according to its normal deterministic destruction semantics.
  • Option and Result do not receive an implicit default merely from their physical representation.
  • None, Ok(default(T)) and Err(default(E)) are not automatically inferred defaults.
  • try treats Some(value) and Ok(value) as success states.
  • try treats None as absence and Err(error) as failure.
  • None can propagate only through a compatible Option return channel unless explicitly handled.
  • Result failure can propagate only through a compatible Result error channel unless explicitly handled.
  • There is no implicit None -> Err(...) conversion.
  • There is no implicit Err(...) -> None conversion.
  • Different concrete Option and Result specializations retain their own generic type identity.
  • Carrier source semantics do not require one universal backend representation.
  • The backend may optimize carrier representation or eliminate temporary carriers only when observable Sec semantics remain identical.