Skip to content

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 }
ExpressionRuntime behavior
Ok(v)Result::Ok(v)
Err(e)Result::Err(AnyError{ payload: e, ... })
expr as Typeinfallible typed conversion via Into<Type>
expr as Type?fallible typed conversion (Result<Type, AnyError>)
value?unwrap success; early-return on failure
lhs !! rhsattach context and return a Result
a ?? bdefault for None
let x = Ok(42);

Err(...) is a normalizer:

  • if payload is already an AnyError, 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 concrete E
let a = Err("disk full");
let b = Err(404);
let c = Err({ code: "IO", path: "/tmp/a.txt" });

None is the missing-value sentinel.

let maybe_id = Some(7);
let missing = None;

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>
}

expr? unwraps and propagates failures.

InputBehavior
Ok(v)yields v
Err(e)early-return Err(e)
Some(v)yields v
Noneearly-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.

as Type is the compile-time checked infallible conversion form.

  • dispatch resolves Into named 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 int

as Type? is the compile-time checked fallible conversion form.

  • conversion compatibility is validated statically on strong types
  • dispatch resolves TryInto named 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?)?

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 Json

See Traits: Conversion Traits for the full derivation rules and guidance on when to use each form.

Shape keeps ? ergonomics compatibility-first:

  • ? does not require matching error generic types between caller/callee
  • ? unwraps the success value (T) from Result<T>/Result<T, E> and Option<T>
  • fallible scopes are wrapped as Result<...> automatically
  • Result<T> remains compatible with Result<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'

lhs !! rhs adds higher-level context and always yields a Result.

lhslhs !! rhs
Ok(v)Ok(v)
Some(v)Ok(v)
Err(e)Err(AnyError { payload: rhs, cause: e, trace_info: single-frame })
NoneErr(AnyError { payload: rhs, cause: AnyError("Value was None"), trace_info: single-frame })
plain value vOk(v)

Example:

let user_id = (find_user() !! "User not found")?;

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

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>

AnyError is represented as a normal object graph (Object + nested cause) and is wire-compatible through shape-wire:

  • Result::Err(any_error) serializes as WireValue::Result { ok: false, value: ... }
  • the embedded AnyError object (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.

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)
}
  • ?. optional property access
  • ?? None coalescing

These compose naturally with ? and !!.

For compile-time borrow/reference diagnostics (B0001-B0004), see References and Borrowing.