Error Handling
Shape supports fallible flow with:
Result(Ok/Err)Option(Some/None)- try propagation operator
? - context operator
!!
The runtime normalizes errors into an AnyError object so Option and Result flow together.
All snippets below assume:
from std::core::intrinsics use { Result, Option, AnyError, Ok, Err }from std::core::from use { From }from std::core::into use { Into }from std::core::try_from use { TryFrom }from std::core::try_into use { TryInto }Quick Reference
Section titled “Quick Reference”| Expression | Runtime behavior |
|---|---|
Ok(v) | Result::Ok(v) |
Err(e) | Result::Err(AnyError{ payload: e, ... }) |
expr as Type | infallible typed conversion via Into<Type> |
expr as Type? | fallible typed conversion (Result<Type, AnyError>) |
value? | unwrap success; early-return on failure |
lhs !! rhs | attach context and return a Result |
a ?? b | default for None |
Constructors
Section titled “Constructors”Ok(value)
Section titled “Ok(value)”let x = Ok(42);Err(payload)
Section titled “Err(payload)”Err(...) is a normalizer:
- if
payloadis already anAnyError, reuse it (idempotent) - otherwise wrap it into a new
AnyError - type-level shape:
Err<T, E>(payload: E) -> Result<T, E>
Ok(...) has matching typed shape:
Ok<T, E>(value: T) -> Result<T, E>Result<T>is shorthand when you don’t care about concreteE
let a = Err("disk full");let b = Err(404);let c = Err({ code: "IO", path: "/tmp/a.txt" });Some(value) and None
Section titled “Some(value) and None”None is the missing-value sentinel.
let maybe_id = Some(7);let missing = None;AnyError Structure
Section titled “AnyError Structure”Runtime errors use an object shape equivalent to:
{ category: "AnyError", payload: <original value>, message: <derived printable message>, cause: <AnyError | None>, trace_info: { kind: "full" | "single", // full -> frames: [...] // single -> frame: {...} }, code: <optional string>}? Try Operator
Section titled “? Try Operator”expr? unwraps and propagates failures.
| Input | Behavior |
|---|---|
Ok(v) | yields v |
Err(e) | early-return Err(e) |
Some(v) | yields v |
None | early-return Err(AnyError) (code OPTION_NONE) |
This makes Option propagation Result-compatible.
? is compile-time restricted to Result<...> and Option<...> operands.
Using ? on a plain non-fallible value is a semantic error.
Typed Conversion
Section titled “Typed Conversion”Infallible: as Type
Section titled “Infallible: as Type”as Type is the compile-time checked infallible conversion form.
- dispatch resolves
Intonamed impl selector from the target type - result type is
Type - user types opt in with
impl Into<Target> for Source as Target { ... }
Example:
let n = true as intFallible: as Type?
Section titled “Fallible: as Type?”as Type? is the compile-time checked fallible conversion form.
- conversion compatibility is validated statically on strong types
- dispatch resolves
TryIntonamed impl selector from the target type - result type is
Result<Type, AnyError> - user types can opt in via named impls (
impl TryInto<Target> for Source as Target { ... })
Example:
fn parse_int(raw: string) -> Result<int> { let n = (raw as int?)? return Ok(n)}Custom conversion examples:
impl TryInto<int> for string as int { method tryInto() { // implementation delegates to std::core::convert (target-side `From` / // `TryFrom` auto-derive `Into` / `TryInto`; see the §Target-Side section // below for the preferred form). The source-side `impl TryInto` form // requires the target-side `From`/`TryFrom` machinery to be wired // through the `Convert` opcode — pending v0.3 G.1 (b)-class follow-up // per `docs/cluster-audits/v0.3-g1-step1-fundamentals.md` §3.B.3. }}
let n = ("42" as int?)?Target-Side: From / TryFrom
Section titled “Target-Side: From / TryFrom”When the target type owns the conversion logic, use From or TryFrom.
The compiler automatically derives the corresponding Into/TryInto on the
source type, so as / as Type? work without extra boilerplate:
type Celsius { degrees: number }
impl From<number> for Celsius { method from(value: number) -> Celsius { Celsius { degrees: value } }}
let temp = 100.0 as Celsius // auto-derived Into<Celsius> on number // (SURFACE: v0.3 G.1 (b)-class §3.B.3 — Convert // opcode trait-dispatch pending §2.7.11 / Q12 // value-call dispatch + AnyError builder. The // user-side `impl Into<Target> for Source as Target` // form is also affected.)impl TryFrom<Json> for string { method tryFrom(value: Json) -> Result<string, AnyError> { match value { Json::Str(s) => Ok(s), _ => Err("Json value is not a string"), } }}
let name = (data.get("name") as string?)? // auto-derived TryInto<string> on JsonSee Traits: Conversion Traits for the full derivation rules and guidance on when to use each form.
Inference Rules for Result/Option
Section titled “Inference Rules for Result/Option”Shape keeps ? ergonomics compatibility-first:
?does not require matching error generic types between caller/callee?unwraps the success value (T) fromResult<T>/Result<T, E>andOption<T>- fallible scopes are wrapped as
Result<...>automatically Result<T>remains compatible withResult<T, E>(success-type-first)
For Err(payload), the success type T is inferred from context:
- explicit return annotations (
fn f() -> Result<int> { ... }) - other branches (for example
Ok(1)in a sibling branch) - downstream constraints after
?
When no context exists at all for Err(...) success-type inference, Shape reports:
Could not infer generic type arguments for 'Result'!! Error Context Operator
Section titled “!! Error Context Operator”lhs !! rhs adds higher-level context and always yields a Result.
lhs | lhs !! rhs |
|---|---|
Ok(v) | Ok(v) |
Some(v) | Ok(v) |
Err(e) | Err(AnyError { payload: rhs, cause: e, trace_info: single-frame }) |
None | Err(AnyError { payload: rhs, cause: AnyError("Value was None"), trace_info: single-frame }) |
plain value v | Ok(v) |
Example:
let user_id = (find_user() !! "User not found")?;!! + ? Parsing
Section titled “!! + ? Parsing”Shape supports the ergonomic form:
lhs !! rhs?parses as(lhs !! rhs)?
Explicit forms still work:
let a = Err("low") !! "high"?;let b = value !! (other_call()?);let c = (find_user() !! "missing user")?;Trace Capture Model
Section titled “Trace Capture Model”Trace capture is optimized:
- root errors (
Err(payload)) capture a full trace once - wraps via
!!capture a single frame each
You still get a full causal chain without repeatedly capturing full stacks.
Uncaught Exception Display
Section titled “Uncaught Exception Display”When an exception escapes all handlers, output uses the unified wire render path:
- ANSI color rendering when terminal capabilities allow it
- plain-text fallback when color/ANSI is disabled (
NO_COLOR,TERM=dumb, etc.) - adapter-based dispatch so custom error object renderers can be registered
Both paths print the same structured causal chain:
Uncaught exception:Error [OPTION_NONE]: high level context at load_config (cfg.shape:7) [ip 29]Caused by: Value was None at read_file (cfg.shape:3) [ip 11]For non-AnyError thrown values, the VM falls back to default value formatting:
Uncaught exception: <value>Transport Over Shape Wire
Section titled “Transport Over Shape Wire”AnyError is represented as a normal object graph (Object + nested cause) and is wire-compatible through shape-wire:
Result::Err(any_error)serializes asWireValue::Result { ok: false, value: ... }- the embedded
AnyErrorobject (payload/cause/trace_info/code/message) serializes recursively as wire objects/arrays/strings/etc.
So AnyError can be transmitted across REPL/server boundaries without a special wire type.
End-to-End Example
Section titled “End-to-End Example”fn read_file(path) { return io_read(path);}
fn load_config(path) { let text = (read_file(path) !! "Failed to read config")?; return parse_config(text) !! "Config parse failed";}
fn main() { let cfg = load_config("config.shape")?; Ok(cfg)}Related Operators
Section titled “Related Operators”?.optional property access??None coalescing
These compose naturally with ? and !!.
For compile-time borrow/reference diagnostics (B0001-B0004), see
References and Borrowing.