Experimental Async/Await on Roku
Async/await support is off by default and intended for controlled Roku pilots. It supports a deliberately limited subset of TypeScript and Hosanna promises; it is not a complete browser or Node.js Promise/event-loop implementation.
Keep it out of per-frame hot paths, do not pass promise objects between SceneGraph interpreters, and do not enable it across an application until its actual workloads have been exercised on target Roku models.
Enable the compiler flag
Enable the feature only in the Roku hsconfig that needs it:
{
"transpileOptions": {
"enableAsyncAwait": true
}
}
Do not add the option to shared platform configs unless every target is intended to use this experimental compiler path. When the option is absent or false, await continues to report HS-1032, the async runtime is not emitted, and existing Roku output follows the established non-async compiler path.
The current Hosanna ESLint preset has a separate no-await-expression rule that does not read this compiler flag. For a pilot, disable that rule only for the reviewed files that use the supported subset:
export default [
{
files: ['src/roku-async-pilot/**/*.ts'],
rules: {
'@hosanna-eslint/no-await-expression': 'off',
},
},
];
Do not disable the rule repository-wide while most application code still relies on the default-off contract. The compiler continues to diagnose unsupported async shapes even when this ESLint rule is scoped off.
Start with a supported shape
Use named module functions or instance methods, and put each await in its own supported statement:
export async function loadRows(source: HsPromise<Array<Row>>) {
const rows = await source;
if (rows.length === 0) {
return rows;
}
const normalized = await normalizeRows(rows);
return normalized;
}
Avoid hiding an await inside a call, condition, loop, switch, or compound expression. Split complex expressions into named intermediate values.
Scheduling behavior
For the documented subset, Hosanna preserves the essential observable async ordering:
- Calling an async function runs its body synchronously up to the first
await. - Every
awaityields, including an await of a plain value or an already-settled promise. - The caller returns before the continuation runs.
- Promise settlement registers or queues the waiting continuation; it never runs the generated step function inline inside
resolve()orreject(). - A per-interpreter FIFO scheduler resumes queued continuations through Hosanna
TimerService.
For example, the trace below must be start, caller returned, resumed 7:
async function example() {
console.log('start');
const value = await 7;
console.log(`resumed ${value}`);
}
example();
console.log('caller returned');
An await is the yield boundary. Once resumed, code runs synchronously until the next supported await or return. A long continuation can therefore block its SceneGraph interpreter and delay rendering or timer work, just as a long JavaScript job can block its thread. The scheduler may drain more queued work at the same checkpoint, so continuously enqueuing continuations can also starve unrelated work.
Each render or Task interpreter owns a separate queue. Do not create a promise in one interpreter and await it in another.
Supported v1 subset
| Area | Supported forms |
|---|---|
| Await statements | await value;, const value = await source;, value = await source;, this.value = await source;, and return await source; |
| Control flow | if / else if / else, early returns, and try/catch, including awaits in catch bodies |
| Functions | Named module functions and instance methods; async functions with no await still return a promise |
| Values | Hosanna HsPromise values, other supported async calls, rejected promises, and plain values |
| Results | then, catch, and finally observers on the returned async-result promise |
| Classes | Instance fields, inheritance, and supported super method chains |
The compiler also supports the existing Hosanna Promise statics, including resolve, reject, all, and race, within the normal supported API constraints.
Unsupported shapes and workarounds
Unsupported forms fail compilation with a specific diagnostic rather than silently producing unsafe BrightScript.
| Unsupported shape | Diagnostic | Rewrite |
|---|---|---|
Await in loops or for await | HS-1151 | Use an approved sequential helper such as runSequentially, or explicit Promise chaining |
Await in switch | HS-1152 | Restructure as if / else |
try/finally around await | HS-1153 | Use .finally() on the returned promise |
foo(await x), an await in a ternary, condition, array, or other expression | HS-1150 | Assign the awaited value on a separate line, then use it |
| Async arrow or function expression | HS-1154 | Extract a named module function or instance method |
| Destructured parameters/declarations/catches or rest parameters | HS-1155 | Assign first and destructure outside the async state-machine path |
Shadowed variables, nested-block var, __hs* names, reserved-word or roLongInteger locals | HS-1155 | Rename or restructure the function |
| Static async methods or nested async declarations | HS-1155 / HS-1035 | Use a named module function |
| More than 200 state-machine blocks | HS-1156 | Split the function |
Also outside the release contract:
- arbitrary third-party thenables;
- promises or awaiters crossing render/Task interpreter boundaries;
- browser APIs, Node.js APIs, or browser-style microtask integration;
- treating the scheduler as a background thread;
- unbounded async work or per-frame animation/game loops.
Promise compatibility limits
Async-result promises queue then, catch, finally, and await reactions in registration order. Awaiting a legacy app-created HsPromise bridges its settlement into the async queue.
For backward compatibility, calling .then() directly on a legacy HsPromise remains synchronous. That is an existing Hosanna behavior and differs from native JavaScript. Code that requires native Promise behavior everywhere is not a suitable pilot for this feature.
Performance expectations
Use async/await for clearer orchestration of genuinely asynchronous operations, not as a performance optimization. On the validated Roku Ultra 4850X, the final two-step async state-machine benchmark measured about 612 microseconds per operation, versus about 604 microseconds for the comparable two-step .then() workload. The async state machine was approximately 1.3% slower, not faster.
Those figures are bounded microbenchmarks from one Roku model, not latency guarantees. The compiler test suite, a framework consumer, a host app, render and Task interpreters, and a bounded device soak passed, but lower-end-device and multi-hour coverage is still required before considering broad or default-on use.
Pilot checklist
Before release:
- scope
enableAsyncAwaitto the Roku project and keep it off elsewhere; - scope the ESLint override to reviewed pilot files;
- keep every await in a documented statement form;
- avoid cross-interpreter promises and per-frame paths;
- test fulfillment, rejection, and ordering on the target Roku models;
- confirm the caller returns before each continuation runs;
- measure frame time, memory, throughput, and long-running behavior for the real workload;
- keep a Promise-chain implementation or flag rollback available.
To roll back, set enableAsyncAwait to false or remove it and replace remaining awaits with the existing HsPromise chaining style. A flag-off build does not include the async runtime.