Status: implemented for the core language, native runtime, and async tasks.
Absolute uses unchecked, typed language exceptions for exceptional failures.
Result<T, E> will remain an ordinary library abstraction for failures that
callers are expected to handle as part of normal control flow. It depends on
the future generic type system and is not the compiler's propagation ABI.
This split keeps constructors, operators, plugin-backed operations and deeply
nested calls usable without changing every return type, while still allowing
explicit Result APIs where failure is an expected outcome.
All thrown objects derive from the compiler-provided Error class. Exceptions
are managed pointers, so a throw transfers ownership to the exception runtime:
class ParseError : Error {
public ParseError(string message) {
this.message = message;
}
}
string load(string source) {
if (source == "") {
throw new ParseError("source is empty");
}
return source;
}
try {
println(load(""));
}
catch (ParseError* error) {
println(error.message);
}
catch (Error* error) {
println("unexpected error");
}
finally {
println("load finished");
}
Rules:
throw expression;requires a managed pointer toErroror a derived class;throw;is a rethrow and is valid only inside acatchblock;- catches are tested from top to bottom; a base catch before a derived catch is rejected as unreachable;
tryrequires at least onecatchorfinallyclause;- the catch binding owns the exception for the lifetime of that catch scope;
- leaving an unmatched handler transfers the same owner to the next handler;
- raw pointers and non-
Errorvalues cannot be thrown; - exceptions are unchecked: function signatures do not contain a
throwslist in the first version.
The initial Error contract contains a string message. The managed runtime
stores a stable dynamic type identifier alongside the allocation handle, so a
base-typed Error* still selects a catch for its derived runtime class. A
cause field and stack trace may be added without changing source-level catch
syntax.
finally executes exactly once after its associated try/catch, including
on normal completion, exception propagation, return, break, and continue.
The first implementation rejects return, break, continue, and throw
inside finally; this prevents cleanup code from silently replacing an active
control transfer. defer uses the same cleanup stack and runs before an
enclosing handler or finally when its lexical scope is exited.
Before an exception leaves a scope, CodeGen performs the existing managed
owner, raw owner, array, and task cleanup for that scope. A caught exception is
therefore not allowed to bypass lifetime checks. The Analyzer merges normal and
caught paths using the same value-flow maps used by if and match.
An exception that reaches main is reported to standard error and terminates
the process with a non-zero exit code after destroying the exception owner.
Absolute does not expose C++ exceptions in generated code. MSVC and the
Itanium-style ABI used by Clang/GCC have different personalities and unwind
metadata, and a native exception must never cross an extern "C" boundary.
Instead, the runtime owns a thread-local exception record containing:
- the managed handle of the thrown
Errorobject; - its stable Absolute type identifier;
- whether an exception is pending.
The compiler emits explicit exceptional control-flow edges:
throwtransfers the managed owner into the runtime record;- local scope cleanups run;
- control branches to the nearest generated handler, or returns through the propagation path when the handler is in a caller;
- potentially throwing Absolute calls test the pending flag before consuming their result;
- a matching catch takes the owner and clears the pending record;
- an unmatched catch leaves the record intact and continues propagation.
The runtime exports absolute_error_set, absolute_error_pending,
absolute_error_type, absolute_error_take, absolute_error_discard, and
absolute_error_report. Managed allocations expose
absolute_managed_set_type/absolute_managed_type for dynamic catch matching.
This produces the same LLVM control flow on Windows and Linux and does not
require target-specific landing pads.
Calls declared with extern "C" are non-throwing from the language's point of
view. Throwing a C++ exception through them is an ABI violation. Syntax and
opaque plugins must also catch native exceptions before returning to the host.
An export "C" definition is the reverse boundary: it exposes an Absolute body
under a native C symbol. The body must handle Absolute errors before returning;
the C ABI does not carry the thread-local pending-error state or native unwind
metadata to its caller.
An async task captures an uncaught Absolute exception in its task record rather
than aborting a worker thread. await transfers that exception to the awaiting
thread and rethrows it at the await expression. Destroying an un-awaited failed
task destroys its captured exception after the existing pending-task diagnostic
has been handled.
- The compiler prepends the built-in
Errordefinition and the parser builds dedicatedThrowStmt/TryStmtnodes. - Analyzer validates catch ordering, ownership, rethrow, and forbidden control
transfer in
finally. - Runtime holds pending errors in thread-local state and task records.
- CodeGen lowers calls, throws, handlers, dynamic type matching, propagation, and cleanup edges without native C++ EH.
awaitrestores task failures on the awaiting thread.
Positive, negative, runtime, cross-function propagation, ownership, async,
Windows, and Linux scenarios are covered by tests/exceptions.abs,
tests/exceptions-errors.abs, and tests/unhandled-exception.abs.