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.
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
errorvalue.
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]
Tis the success type;Eis the error-channel type;- the active value is either
Ok(T)orErr(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
errorroot; - whether
Ok()orErr()consumes the receiver; - the result type of a projection;
- the borrow created by
OkReforErrRef; - the source location that consumed a carrier before an invalid later use;
- the correct
Some/NoneorOk/Errpatterns; - the alternate state that a later
trywill 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
errorwhen 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,OkandErr. - Return an already-compatible complete carrier directly instead of unpacking and rebuilding it.
- Use
matchwhen both carrier states should be explicit in source. - Use
Ok()orErr()only when consuming the complete Result is intended. - Use
OkRef,ErrRefor borrowed matching when the Result must remain available. - Do not ignore a Result-producing operation accidentally.
- Use
discardonly when both possible active payloads may legally be discarded. - Do not expect carrier construction or projection to clone move-only payloads.
- Do not use
Optionas a disguised error channel when callers need failure information. - Do not use
Resultmerely to represent ordinary optionality. - Do not convert between
NoneandErrimplicitly; state the mapping explicitly. - Use
Option[ref T]rather than nullable safe references.
Summary
Option[T]andResult[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)andNone. Noneis not an error and carries no hidden error value.- Optional safe references use forms such as
Option[ref T]andOption[ref mut T]. - Safe references themselves remain non-null.
Some(value)explicitly constructs a present Option value.Noneexplicitly 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
Tis not implicitly wrapped inSome(T). Result[T, E]has exactly two generic arguments.Tis the success type.Eis the declared error-channel type.Emust beerroror a concrete Sec error type.- The two Result states are
Ok(T)andErr(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 forResult[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
Tis not implicitly converted toResult[T, E]. - Option matching uses
Some(...)andNone. - Result matching uses
Ok(...)andErr(...). - Option and Result pattern families are not interchangeable.
matchis the general explicit construct for handling both carrier states.- Result has compiler-known consuming projections
Ok()andErr(). result.Ok()returnsOption[T].result.Err()returnsOption[E].Ok(value).Ok()becomesSome(value).Err(error).Ok()becomesNone.Err(error).Err()becomesSome(error).Ok(value).Err()becomesNone.Ok()andErr()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
OkRefandErrRef. OkRefreturnsOption[ref T].ErrRefreturnsOption[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))andErr(default(E))are not automatically inferred defaults.trytreatsSome(value)andOk(value)as success states.trytreatsNoneas absence andErr(error)as failure.Nonecan 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(...) -> Noneconversion. - 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.