← Back to AILP Home

Errors

Async functions use the same pass / fail terminator contract as sync functions. The difference is what they resolve to: a sync pass v returns v to the caller; an async pass v constructs a success Result<T> and stores it to the coroutine promise, where the awaiter picks it up.

pass and fail in async bodies

async func:get_value = int32() {
    pass 42i32;          // success → Result<int32>{ value=42, is_error=false }
};

async func:get_error = int32() {
    fail 99i32;          // error   → Result<int32>{ error=99, is_error=true }
};

Both pass and fail short-circuit the rest of the body. Code after the terminator does not execute. This is the v0.31.0.3 D-7 contract:

async func:short_circuit = int32() {
    fail 7i32;
    println("DEAD");     // unreachable; compiler warns [dead-code]
    pass 0i32;           // unreachable
};

Regressions bug339bug342 cover:

The compiler emits a [dead-code] warning on the unambiguously unreachable cases (bug339 / bug340 / bug342). The while-body case (bug341) is conservatively reachable from the post-loop point of view and is not warned.

failsafe is not invoked for fail inside async

func:failsafe = int32(tbb32:err) {
    println("FAILSAFE");   // does NOT fire for the inner fail below
    exit 1;
};

async func:inner = int32() {
    fail 7i32;             // resolves the future, does NOT escape to failsafe
};

async func:caller = int32() {
    Result<int32>:r = await inner();
    if (r.is_error == true) {
        println("CAUGHT-VIA-RESULT");
    }
    pass 0i32;
};

A fail inside an async func: body produces an error Result<T> at the awaiter. It does not propagate up to main's failsafe. The error is delivered to the code that wrote await inner().

This is intentional and matches the v0.21.x error-handling philosophy: errors are values, not control-flow exceptions. Result<T> carries the error; failsafe only triggers for errors that reach the top of main synchronously.

If you want a fail deep in an async chain to terminate the program, propagate it explicitly through each await:

async func:deep = int32() { fail 7i32; };

async func:mid = int32() {
    int32:v = raw await deep();   // `raw` traps on is_error → terminates
    pass v;
};

Or unwrap with ?:

async func:mid = int32() {
    int32:v = (await deep())?;    // `?` propagates fail to the caller's Result
    pass v;
};

await outside async: ARIA-040

Since v0.31.0.1 (D-3), await outside an async func: body is a hard compile-time error:

func:main = int32() {
    int32:v = raw await get_value();   // ERROR
    exit v;
};
ARIA-040: 'await' may only appear inside an 'async func:' body

Before v0.31.0.1, this was a soft-fail (stderr + exit 0). Code in the wild that relied on the old behaviour will now stop compiling — by design. The fix is to either make the caller async (and arrange for it to be drop-spawned or await-chained), or to drop-spawn the call and synchronise some other way.

Regression: bug334 (must fail with ARIA-040); bug335 / bug336 cover the executor-drain interaction (the success side).

No try / catch

catch was tokenised in earlier cycles and was already rejected by the v0.21.1 A-014 diagnostic with a redirect to defaults / ? / !! / pick. The v0.31 PLAN proposed bringing it back as an await-site error handler. The v0.31.0.0 audit (D-8) recommended Option A — pull catch from the language surface entirely — and v0.31.0.2 shipped that decision.

After v0.31.0.2:

Regressions: bug337 (catch as identifier compiles and runs; exit code 7), bug338 (try/catch still rejected, just via the generic path). The pre-existing bug088 / bug098 diagnostics still mention catch in their error messages — they get it via the source-context line, not a dedicated keyword check.

The substitutes:

You wanted… Use
Fallback value on error defaults
Propagate error to caller ?
Trap on error (terminate) raw or !!
Structured error handling pick err.code { ... }
Capture error and continue Result<T>:r = await x; if (r.is_error == true) { ... }