Exceptions

Xenon exceptions carry ordinary typed values through native stack unwinding. Catch handlers select by Xenon type, live values keep deterministic cleanup, and finally runs on every exit from its protected region.

Throwing a value

throw expression; constructs an exception object from the expression and transfers control to the nearest matching handler. There is no required base exception type: primitives, enums and concrete value structs may be thrown when their normal copy or move rules permit it.

throw 42;
throw ParseError();
throw move error;

A named non-copyable local still requires an explicit move. The exception owns its stored value until it is handled or reported as unhandled, and its destructor runs exactly once.

Not every storage view is throwable

void, null, references, raw pointers, arrays, storage, pin and atomic wrapper values are rejected as thrown types. Throw the underlying stable value instead.

Handling by type

A typed handler receives a readonly reference to the active exception object. Its lifetime ends with the handler, so it cannot escape. Struct matching includes base structs; primitive and enum matching is exact.

try
{
    LoadConfiguration();
}
catch (readonly FileError& error)
{
    Log(error.Code);
}
catch (...)
{
    LogUnknownFailure();
}

Handlers are tested from top to bottom. Put derived struct handlers before their base handlers and keep catch (...) last; a handler hidden by an earlier one is a compile error.

Rethrowing

A bare throw; is valid only inside a catch handler. It continues propagation with the same active exception object and preserves its dynamic Xenon type.

catch (readonly ParseError& error)
{
    Record(error);
    throw;
}

Guaranteed finalization

A try must have at least one catch or a finally. The finalizer runs exactly once after normal completion, after a handled or propagating exception, and before a return, break or continue leaves the protected region.

try
{
    Use(resource);
}
finally
{
    Release(resource);
}

Lexical locals in the protected block are destroyed before the finalizer runs. If the finalizer throws while another exception is propagating, the new exception replaces the previous one and propagation continues with the new value.

Native and library boundaries

Exception control flow is stored in XELIB bodies, and type identities remain stable when a library throws a value that a consuming project catches. Compatible Xenon executables and shared libraries use one process runtime with per-thread active-exception state, so propagation and matching remain coherent across native module boundaries on Windows, macOS and Linux.

An exception leaving the generated executable entry point or an exported C ABI entry point is treated as unhandled. Xenon prints the fully qualified thrown type to standard error, completes exception cleanup and terminates with a non-zero status. Ordinary extern C functions are not treated as Xenon exception sources.

Destructor exceptions

A destructor called during ordinary execution may throw. This includes lexical scope exit, explicit destruction, virtual destruction, base and field destruction, array elements and ownership or storage<T> cleanup. The exception propagates through the normal matching and cleanup rules.

If a destructor throws while another exception is already unwinding, Xenon terminates the process after deterministic exception cleanup. Direct and virtual destructors follow the same rule; a second exception never replaces an exception from an active unwind.