Skip to content
Noodle
InstallLearnPlayground
GitHub

Asynchronous code

Use asynchronous functions when work may suspend while waiting for a platform operation or another task. Noodle keeps success, business failure, and cancellation as distinct typed outcomes.

errortype LoadError
LoadFailed(String)
end
async func load_count() throws LoadError -> Int do
42
end

The declared body result is Int. Callers receive Task[Int, LoadError]. An async function without throws uses the empty standard-library error set NoError.

Calling an async function starts its task; it does not implicitly wait for the result.

.await() is available only inside an async context:

errortype LoadError
LoadFailed(String)
end
async func load_count() throws LoadError -> Int do
42
end
async func doubled_count() throws LoadError -> Int do
outcome = load_count().await();
switch outcome
case AsyncResult::Ok(value) then value * 2
case AsyncResult::Err(error) then throw error
case AsyncResult::Cancelled(_) then 0
end
end

Waiting produces AsyncResult[T, E] with three constructors:

  • Ok(T) for success;
  • Err(E) for the task’s declared business error;
  • Cancelled(CancelReason) when the task is abandoned.

Cancellation is not merged into the error set.

Postfix ! on AsyncResult extracts Ok and propagates both Err and Cancelled through the enclosing async function:

errortype LoadError
LoadFailed(String)
end
async func load_count() throws LoadError -> Int do
42
end
async func doubled_count() throws LoadError -> Int do
value = load_count().await()!;
value * 2
end

Use this form when the current function does not own recovery. Use an explicit switch when each outcome needs local behavior.

An entry file may declare async func main() -> Unit. The program runner waits for it:

errortype LoadError
LoadFailed(String)
end
async func load_count() throws LoadError -> Int do
42
end
async func doubled_count() throws LoadError -> Int do
value = load_count().await()!;
value * 2
end
async func main() -> Unit do
result = doubled_count().await();
switch result
case AsyncResult::Ok(value) then Debug.trace(value)
case AsyncResult::Err(LoadError::LoadFailed(message)) then
Console.println(message)
case AsyncResult::Cancelled(_) then Console.println("cancelled")
end;
end

At the outer entry boundary, success exits normally, an unhandled business error exits unsuccessfully, and cancellation uses the cancellation exit path.

An async binding starts its initializer immediately in a background task. Its type is AsyncResult[T, E], and referencing the binding waits for that task; the reference is legal only inside an async context:

async func load_name() -> String do
"Ada"
end
async func greeting() -> String do
async name = load_name().await()!;
"Hello, " ++ name!
end

The initializer is checked as an anonymous async body, so it may use .await() and postfix !. A plain value is lifted to AsyncResult::Ok; errors propagated inside the initializer become that task’s error outcome. The compiler reports an async binding that is never referenced.

Multiple bindings start before the rest of the block runs, so they can overlap:

async func load_user() -> String do "Ada" end
async func load_plan() -> String do "Pro" end
async func describe() -> String do
async user = load_user().await()!;
async plan = load_plan().await()!;
user! ++ " / " ++ plan!
end

Use an async binding when the lexical scope clearly owns the concurrent work. Call an async function directly when explicit task ownership and waiting make the control flow easier to see.

Cancellation is cooperative. Waiting on a child task registers cancellation through the wait chain, but a long synchronous computation is not interrupted in the middle of a segment. Cancelling a settled task has no effect, and repeated cancellation is idempotent.

Cancellation behavior is experimental and may change as the runtime model stabilizes. In particular, an unconsumed async binding is cancelled when its lexical block exits; consumed tasks are unaffected, and cancellation is idempotent.

Task-producing extern func declarations form the platform boundary. Their host companion must bridge the platform promise into the standard Task model so cancellation metadata and AsyncResult semantics remain intact.

Next: Tests, including asynchronous tests.