Calling and awaiting
An E# function becomes asynchronous when its body contains await. There is no async keyword. What
matters at a call site is whether you want to suspend, keep a synchronous boundary, or pass a CLR task on.
Three call-site choices
Section titled “Three call-site choices”| Need | Write | What happens |
|---|---|---|
| Release the current thread while waiting | let x = await load() | The caller becomes async and resumes later. |
| Keep a synchronous signature | let x = load() | For an uncolored E# async function, work starts now and joins when x is first read. |
| Pass or store a task explicitly | declare -> Task<T> / -> ValueTask<T> | The call yields that CLR task value; handle it explicitly. |
func load(id: int) -> int { return await Task.FromResult(id * 10)}
func asyncPath() -> int { let value = await load(4) // suspend; asyncPath returns ValueTask<int> return value + 2}
func syncPath() -> int { let value = load(4) // starts load immediately let label = "rendering" // independent work can proceed return value + label.Length // first read synchronously joins load}Force-on-use is a blocking boundary
Section titled “Force-on-use is a blocking boundary”let value = load() is not fire-and-forget. It keeps the ValueTask<T> privately, then emits a
GetAwaiter().GetResult() join immediately before the first use of value. Exceptions therefore surface at
that use, and the caller’s thread is held only for the unfinished remainder.
Use it for command-line programs, UI orchestration, and adapter layers where a synchronous signature is more
valuable than thread scalability. Use await in request-serving or high-concurrency paths: it releases the
thread instead of parking it.
Explicit wrappers stay explicit
Section titled “Explicit wrappers stay explicit”The bridge applies only when an E# async function declares an ordinary result type. An explicit wrapper is a promise to expose the CLR async shape:
func loadForApi() -> Task<int> { return await Task.FromResult(42)}
func caller() -> int { let task = loadForApi() // task is Task<int>, not a deferred int return task.GetAwaiter().GetResult()}That distinction is useful at interop boundaries: C# interfaces and BCL APIs can receive exactly the
Task<T> or ValueTask<T> they require. See the specification
for the precise rule.