Skip to main content

Experimental Async/Await on Roku

Experimental and opt-in

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:

  1. Calling an async function runs its body synchronously up to the first await.
  2. Every await yields, including an await of a plain value or an already-settled promise.
  3. The caller returns before the continuation runs.
  4. Promise settlement registers or queues the waiting continuation; it never runs the generated step function inline inside resolve() or reject().
  5. A per-interpreter FIFO scheduler resumes queued continuations through Hosanna TimerService.

Roku async lifecycle running to await, returning to the caller, queueing a continuation, and resuming later through the per-interpreter FIFO schedulerRoku async lifecycle running to await, returning to the caller, queueing a continuation, and resuming later through the per-interpreter FIFO scheduler

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

AreaSupported forms
Await statementsawait value;, const value = await source;, value = await source;, this.value = await source;, and return await source;
Control flowif / else if / else, early returns, and try/catch, including awaits in catch bodies
FunctionsNamed module functions and instance methods; async functions with no await still return a promise
ValuesHosanna HsPromise values, other supported async calls, rejected promises, and plain values
Resultsthen, catch, and finally observers on the returned async-result promise
ClassesInstance 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 shapeDiagnosticRewrite
Await in loops or for awaitHS-1151Use an approved sequential helper such as runSequentially, or explicit Promise chaining
Await in switchHS-1152Restructure as if / else
try/finally around awaitHS-1153Use .finally() on the returned promise
foo(await x), an await in a ternary, condition, array, or other expressionHS-1150Assign the awaited value on a separate line, then use it
Async arrow or function expressionHS-1154Extract a named module function or instance method
Destructured parameters/declarations/catches or rest parametersHS-1155Assign first and destructure outside the async state-machine path
Shadowed variables, nested-block var, __hs* names, reserved-word or roLongInteger localsHS-1155Rename or restructure the function
Static async methods or nested async declarationsHS-1155 / HS-1035Use a named module function
More than 200 state-machine blocksHS-1156Split 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 enableAsyncAwait to 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.

Talk to us