STAGE 2 / Language semantics · LESSON 13
Why this exists
Async functions make pending results explicit while an application waits for external work. The owner still needs to await them, handle rejection, and decide when the overall run is complete.
Runtime contract
An async func produces an awaitable result. await blocks expression completion until the pending value resolves. This does not mean every statement runs concurrently, or that an async declaration automatically accelerates CPU-bound work.
The example uses pure arithmetic inside an async function so scheduling semantics can be observed without a timer or network dependency. The caller awaits the result and asserts it before announcing completion. The failure drill awaits a rejected operation and lets the error reach the command boundary.
Lifecycle
Keep a clear owner for required work. If a run's success depends on a value, await it before producing the receipt. Bound the input and use the runtime's finite scheduler deadline as a backstop, not as your only application stop condition.
Native async I/O still has effects. An async file read needs filesystem authority; an async HTTP call needs network authority. Scheduling does not bypass capability gates or make returned data trustworthy.
Professional pattern
Separate a pure transformation from the asynchronous acquisition of its inputs. This lets deterministic tests cover the business rule without timing. Add a smaller integration test for completion and error propagation. Do not use sleeps as a substitute for waiting on owned work.
Common mistakes
Printing a handle is not inspecting the completed value. Starting work and immediately declaring success loses evidence. Catching only the call site while ignoring the awaited failure can also miss the actual error boundary. The next lesson distinguishes bounded mapped work from detached spawn.
Working example
async func double(value) { return value * 2 }
let pending := double(5)
let answer := await pending
assert_equal(answer, 10)
print(answer)
Run it
From the course repository root, use the pinned Kujo 1.3.1 runtime.
kujo check examples/13.kujo
kujo run --untrusted examples/13.kujo
Captured output
10
Break it and diagnose it
The awaited operation rejects. Completion must propagate the failure rather than produce a success receipt.
async func fail() { throw("async operation rejected") }
let result := await fail()
print(result)
kujo run --untrusted examples/13-break.kujo
Exit status: 4. Captured diagnostic:
[KUJOVM001] [vm] Runtime Error: async operation rejected
--> 0:0
Call stack:
0 at fail
Exercise
Create an async transform and a synchronous pure equivalent. Assert equal successful results and exercise a rejection. Draw which function owns the pending value and where success may be emitted.
Checkpoint
- I distinguish a handle from its resolved result.
- I await work required for success.
- I preserve async failures.
Verified 2026-09-06 · Official Kujo 1.3.1 release · Source contract · Download example