This is a request for comments on an experimental TypeScript fork. Buffered

writes usually complete immediately and occasionally need an asynchronous flush.

Repeated const temp = ...; if (temp) await temp checks are painful and ugly;

await? expresses that sequencing directly. async? preserves synchronous

completion through the calling layers, returning a Promise only when needed.

Latest measurements — layered buffered writer: 100,000 records, 300,000 writes, 4.8 MB; pooled medians of 27 samples across three fresh Node processes. The same latest batch is used for every entry. Lower times are better.

In this batch, conditional functions take about 4.6× less elapsed time than

ordinary async/await in memory, and are within about 2% of the manual

continuation median. Manual continuations explicitly resume through

pending.then(resume) after a flush; they assume void | Promise<void>, while

the compiler supports arbitrary thenables. For the flat in-memory writer,

async? + await? takes 2.74 ms, versus 20.16 ms for ordinary async/await.

Supported short sequences and for loops create continuation callbacks only

when they suspend; larger sequences use shared callbacks, and complex bodies

retain a generator fallback. This is a working prototype, not an ECMAScript

standard or an upstream TypeScript feature.

Measured on Node v24.19.0, targeting ES2022, on 2026-10-09. These synthetic

results vary in a shared environment; file writes omit fsync. They do not

predict native-engine performance or establish statistical equivalence. See

the full latest results and tradeoffs

for both layouts, earlier comparisons, raw samples, and limitations.

Buffered code often performs many small operations that complete immediately

in memory. Only occasionally does the buffer fill and require an asynchronous

flush. A useful API therefore returns void | Promise<void>: no Promise when

the write fits, and a Promise when the caller must wait for flushing before

continuing. Similar patterns occur with cached values and composable stream

processing.

Today, preserving the synchronous path at each call site means repeatedly introducing temporary variables and conditional awaits. For example, inside an ordinary async function, writing the parts of a record becomes:

const headerResult = buffer.write(header);

if (headerResult) await headerResult;

const payloadResult = buffer.write(payload);

if (payloadResult) await payloadResult;

const trailerResult = buffer.write(trailer);

if (trailerResult) await trailerResult;This check is appropriate for the specific void | Promise<void> contract;

general value-or-thenable APIs need a proper thenable check instead. The

temporary variables avoid evaluating each operation twice, but repeating

const temp = ...; if (temp) await temp throughout a pipeline is painful and

ugly. It buries the sequence of actual work under bookkeeping and makes every

new operation another opportunity to forget the completion check.

Writing await buffer.write(...) is concise, but introduces a suspension even

when the result is undefined. Writing conditional awaits manually avoids

those suspensions, yet an ordinary async enclosing function still always

returns a Promise. Preserving synchronous completion through several layers

requires further branching, explicit continuations, or a generator runner.

The proposed spelling makes the intended sequence visible:

await? buffer.write(header);

await? buffer.write(payload);

await? buffer.write(trailer);Inside an async? function, these operations can complete synchronously all

the way back to its caller, or return a Promise when an actual suspension is

needed. The aim is to express a deliberately mixed completion contract once,

without duplicating the synchronous and asynchronous algorithms or scattering

temporary-variable checks through every layer. This changes observable timing;

it is not a transparent replacement for ordinary async/await.

We measured the motivating pattern rather than assuming that prettier syntax

must produce faster code. Skipping unconditional awaits helps, but tiny

layered async? functions expose substantial generator overhead. The original

results below motivated a lowering optimization; the follow-up measurements

and explanation immediately after these tables describe the updated output.

Latest result: delaying callback creation reduces layered in-memory async?

from 9.00 ms to 6.04 ms. In the same new batch, ordinary async/await takes

27.52 ms and manual continuations take 5.94 ms. The generated and manual

in-memory medians are now within about 2%; this is not a statistical equivalence

claim. The complete latest tables and earlier batches below show the gains,

remaining costs, and variation.

“Manual sync continuation” is hand-written JavaScript control flow: run ordinary

calls and loops while results are undefined; when a write returns a Promise,

return pending.then(resume) to continue after flushing. It creates no generator

and does not suspend for synchronous writes. The layered version has explicit

payload/trailer callbacks. This benchmark implementation assumes its precise

void | Promise<void> contract, whereas the compiler must handle arbitrary

thenables, getter effects, and error timing.

Each trial writes 100,000 records / 300,000 small writes / 4.8 MB. Periodic

scenarios flush a 64 KiB buffer (73 full flushes plus one final partial

flush). The microtask sink isolates scheduling overhead; the file sink actually

writes and verifies the complete output, without fsync. The in-memory case

fits the entire input in its buffer and does not flush.

Flat writer: one function loops over all writes. Lower times are better; entries are pooled medians of 27 samples across three fresh Node processes.

Layered writer: the outer loop calls a separate header/payload/trailer function for each record. Every variant waits for completion where required; the byte sequence and total work are identical to the flat case.

In the flat in-memory case, ordinary async plus await? took 4.06 ms

versus 20.35 ms for unconditional awaits, about 5.0× less elapsed time.

The existing manual checks were faster still at 2.35 ms. async? plus

await? took 14.65 ms, offering synchronous return semantics at a higher

cost than manual continuation code (2.64 ms).

The layered in-memory case is the clearest current limitation: async? plus

await? took 155.06 ms, about 4.3× the elapsed time of ordinary

async/await (35.81 ms). It was also slower in both layered flush scenarios.

This is consistent with substantial generator/runner/probe overhead for tiny

functions; these measurements do not isolate the cost of each component.

Ordinary async record functions still return Promises even with await?, so

conditional awaiting at the outer layer cannot make that path synchronous.

These original results support the motivation to avoid needless suspension and repeated

checks, while identifying efficient conditional-function lowering as an open

implementation problem. The manual continuation variant shows that the

synchronous contract is feasible with existing JavaScript; the proposed syntax

makes that control flow easier to author, but the original lowering did not

match its performance. Only async? and the manual continuation variant

returned undefined synchronously in the no-flush trials; all strategies

returned a Promise when a flush was required. The variants therefore share

ordered output, not identical scheduling/API contracts.

Measured on 2026-10-09, Node v24.19.0, V8 13.6.233.17-node.51, Linux x64, Intel Xeon Platinum 8573C, with nine exposed logical CPUs and an overlay filesystem. Output target: ES2022 CommonJS, with all variants compiled by the same fork. Each process used three warmup rounds and nine measurement rounds per strategy/scenario, rotating order. Compilation, buffer setup, file open/close, and output verification were outside the timer; final flushes, allocations by the writers, GC, and completion waits were inside.

This shared environment has substantial sample variation. For example,

layered async? microtask trials ranged from 131.01 to 300.63 ms; we kept

all samples. The file results include OS caching and do not measure durable

physical-disk throughput. These are synthetic buffered-writing workloads,

not a browser benchmark, a full application workload, or a prediction of native

engine performance. We request independent replications and workload-specific

measurements, especially for alternative lowerings.

See benchmark source, method, and reproduction commands, all raw runs, run 2, run 3, and pooled medians and ranges.

The layered writer writes the same bytes, but calls a three-write record function

100,000 times instead of doing all writes inside one function. In the original

conditional output, each record call constructed a generator function, a

generator object, and a runner closure. Every plain await? still created a

probe array and a tagged yield packet, yielded a generator result, and called

back into generator.next. Three inner awaits plus one outer await meant

400,000 such yield/resume cycles even when every write returned undefined.

Each completed record also went through return-value probing.

This is compiler-generated work, not extra buffered output or more flushes. The flat variant amortizes its generator setup across the whole loop; the layered variant pays that setup per record. This explains the structural source of the regression. We have not attributed individual milliseconds to allocations, generator calls, JIT decisions, or garbage collection with an allocation/CPU profile.

The updated lowering makes three changes:

- Probe before yielding. Plain conditional awaits continue inside the generator

without a yield/resume cycle; the probe returns nullwithout allocating an array. Thenables retain receiver-preserving wrappers and Promise assimilation.

- For a standalone await?, discard the result without retaining a separate value temporary. A value-consuming await still retains its plain result.

- At completion, return immediately for undefined; no completion probe or wrapper is needed. Other return values still undergo thenable assimilation.

The runner consequently needs no tagged packets or synchronous resumption

loop. Ordinary await still yields unconditionally. Error timing, single operand

and getter evaluation, rejection handling, and ordered buffer reuse are preserved.

Void handling works when applied to discarded values and actual undefined

completion. A static void annotation alone is insufficient. TypeScript can

accept a value-returning callback where () => void is expected. A function

that returns that callback's result can therefore return a runtime thenable

even though the expression has static type void. Skipping its assimilation

would change completion and rejection behavior. Likewise, ignoring an awaited

value never licenses skipping the wait itself. The optimizations depend on

syntax and runtime values, without making type annotations change JavaScript

behavior. Tests cover discarded fulfillment/rejection and a contextual-void

callback that returns a plain value, resolved Promise, or rejected Promise.

We reran the unchanged workload in three fresh processes, pooling 27 samples

per case. The original raw results are retained separately. The following table

compares the async? + await? medians before and after the change; lower

is better. Speedups compare separate measurement batches, not paired trials.

For comparison, these are all strategies in the updated batch:

Flat writer

Layered writer

The flat case improves substantially; the layered case improves but still loses

to ordinary async/await and manual continuations. Generator-function/object and

runner setup still occur per record. These results motivated the direct continuation lowering described next.

General-purpose continuation/state-machine lowering still must preserve locals,

lexical bindings, try/finally, loops, and synchronous error behavior; complex

bodies retain the generator fallback. The shared environment remains noisy: updated layered

microtask samples span 89.76–322.64 ms. Other strategies' medians also moved between

batches, so these numbers support an implementation improvement, not a precise

universal speedup. See optimized-run-{1,2,3}.json and optimized-summary.json

under tools/benchmarks/conditional-async/ for all raw samples and ranges.

We now generate ordinary continuations for the benchmark's three-write record function and ordinary resume loops for its outer/flat loops. The synchronous path no longer constructs generator functions, generator objects, or runners. It still uses receiver-preserving thenable probing; only actual suspension calls the Promise continuation helper. This is the same broad execution pattern as the manual implementation, with the proposal's broader thenable semantics.

The unchanged workload was rerun in three fresh processes, with 27 pooled samples per case. These are all strategies in the first direct-generation batch:

Flat writer

Layered writer

Layered async? before/after direct generation:

The remaining difference from manual continuations includes general thenable probes, wrapped Promise normalization on suspension, discarded final fulfillment handling, and differences in the generated closure/control-flow shape. We have not separately profiled these costs, and matching handwritten code's exact speed is not guaranteed. The flat writer was already amortizing generator setup, so removing it yields a much smaller improvement there.

These before/after ratios compare separate batches. Other strategies moved

between batches, so the same-batch comparison is the stronger guide to relative

performance. We retained every sample, including outliers: direct layered file

trials range from 10.24 to 95.76 ms. Environment and method are unchanged;

these are synthetic Node workloads, not native-engine predictions. See

continuation-run-{1,2,3}.json and continuation-summary.json alongside the

previous results for all raw timings and ranges.

For short sequences of up to four discarded awaits, synchronous work is now inline. Each thenable branch creates an arrow for the remaining work; the plain branch continues inline. This duplicates suffix code across branches, so the optimization is limited to four awaits. Longer direct sequences retain shared callbacks to bound code size. Supported loops also start inline: the first suspension creates one resume callback, which is reused for subsequent flushes. Thus the motivating three-write record and its outer loop create no continuation closures on wholly synchronous calls. This does not claim zero engine allocations: activation/context storage can still cost memory.

The same workload and settings were rerun in three fresh processes, with 27 pooled samples per case and no discarded observations. All strategies in the latest batch:

Flat writer

Layered writer

async? before/after delayed callback creation:

The reduction is largest in the layered in-memory case, where every record

previously created shared continuation closures. The same-batch manual result

is now close in memory, while it remains faster with periodic flushes. The flat

file median increased slightly; this change does not win every measured case.

The before/after percentages compare separate batches and should not be read as

precise causal estimates. Other strategies also moved between batches, and

updated layered file samples span 9.44–21.50 ms. Independent replication is still

needed. All raw data and ranges are retained in lazy-run-{1,2,3}.json and

lazy-summary.json under tools/benchmarks/conditional-async/.

The semantics remain unchanged: operand and getter evaluation, receiver

preservation, nested thenable assimilation, discarded fulfillment, rejection,

lexical this, and post-fulfillment loop increments retain their behavior.

Tests cover suspension at each record write, all-thenable sequences, loop

resumption, no Promise allocation on plain paths, and the retained shared

continuation path for a five-await sequence.

This document is a request for comments, backed by an experimental compiler implementation so the semantics and emitted code can be examined and tested. The design is open to revision. We want feedback on:

- Whether buffered writes, caching, and similar workloads justify this syntax rather than ordinary async functions, manual branching, or library helpers.

- Whether async?andawait?communicate conditional completion clearly, and whether a different spelling or contract would be easier to reason about.

- Whether the specified timing, reentrancy, error, and thenable behavior is acceptable, and which concrete examples reveal unsafe or surprising behavior.

- Whether the types, declarations, generated code, and tooling constraints make this useful in practice, and what implementation cases we have missed.

- Measurements on real workloads, including comparisons with hand-written branching, ordinary async/await, and alternative library approaches.

The historical-objections section below records both our mitigations and the tradeoffs that remain. We are asking reviewers to evaluate those tradeoffs, not assuming that an implementation resolves the language-design objections.

This experimental compiler fork adds adjacent async? and await? suffixes.

It is based on microsoft/TypeScript main at

6ad8c56f9b5a9bb910046c56059296311adc24ba.

async? function write(events: Event[]): void | Promise<void> {

for (const event of events) {

await? buffer.write(event)

}

}

const result = write(events)

// result is undefined when the whole operation completes synchronously,

// or a Promise<void> once a thenable requires suspension.await? expression evaluates the operand once. It checks callable then

on objects and functions; a plain value continues immediately without a

microtask boundary. A thenable suspends execution and its fulfillment or

rejection resumes the surrounding function. The then getter is read once

for that await operation, and a thenable method receives its original receiver.

async? supports function declarations, expressions, arrows, and class and

object methods. The body starts synchronously and continues synchronously

through plain conditional awaits. On the first actual suspension, the call

returns a native promise. Later plain conditional awaits also continue without

adding a suspension. A regular await always suspends, including when its

operand is a plain value.

An exception before suspension is thrown synchronously. An exception after

suspension rejects the returned promise. try, catch, and finally retain

their normal control flow. A returned thenable is assimilated to a native

promise, even when no await was reached. The function's parameter defaults

execute synchronously as part of starting the body.

The checker conservatively infers Awaited<T> | Promise<Awaited<T>>

(or void | Promise<void>).

It does not try to prove that a particular path never suspends. An explicit

return annotation must accept both synchronous and asynchronous completion.

Declaration emit removes the modifier and exposes that union to consumers;

consumers of the generated declarations do not need this fork.

await? also works inside ordinary async functions and async generators, and

at the top level of modules with the same target/module restrictions as normal

await. Ordinary async functions continue to return promises. async? generators

are rejected because this extension does not define a conditional iterator API;

use ordinary async function* for them.

The emitter selects direct continuations for eligible discarded-await sequences and single-await for loops; other bodies use generators and a small runner, using TypeScript's existing preservation of lexical bindings and control flow. Plain conditional awaits allocate no probe/yield packets or Promise and introduce no microtask. The fallback still allocates a generator; the direct path uses ordinary continuations instead. Short sequences and supported loops create callbacks only after an actual suspension is required; larger sequences still allocate shared callbacks up front. Performance depends on body shape and workload; the measurements above distinguish those paths.

The new helpers are emitted inline even with --importHelpers, because released

tslib packages do not supply them. --noEmitHelpers suppresses them and requires

the application to supply equivalent helpers. Source AST printing preserves the

suffixes; emitted JavaScript contains standard JavaScript only.

These examples were compiled with this fork using --target es2022.

The declaration supplies the type of a separately implemented buffer writer;

it produces no JavaScript. Formatting below matches the compiler output.

declare function writeChunk(chunk: string): void | Promise<void>;

async? function writeAll(chunks: string[]): void | Promise<void> {

for (const chunk of chunks) {

await? writeChunk(chunk);

}

}

async function ordinaryAsync(chunk: string): Promise<void> {

await? writeChunk(chunk);

}The emitted functions (the leading "use strict" is omitted):

function writeAll(chunks) {

return __conditionalAwaiter(this, void 0, void 0, function* () {

var _a;

for (const chunk of chunks) {

((_a = __conditionalAwait(writeChunk(chunk))) ? yield _a : void 0);

}

});

}

async function ordinaryAsync(chunk) {

var _a;

((_a = __conditionalAwait(writeChunk(chunk))) ? await _a : void 0);

}The compiler emits these helpers once per output file when needed:

var __conditionalAwait = (this && this.__conditionalAwait) || function (value) {

var then = value !== null && (typeof value === "object" || typeof value === "function") ? value.then : void 0;

return typeof then === "function" ? { then: function (resolve, reject) { Reflect.apply(then, value, [resolve, reject]); } } : null;

};

var __conditionalAwaiter = (this && this.__conditionalAwaiter) || function (thisArg, _arguments, P, generator) {

function step(result) {

if (!result.done) {

return Promise.resolve(result.value).then(

function (value) { return step(generator.next(value)); },

function (error) { return step(generator["throw"](error)); }

);

}

// A bare return/fallthrough needs neither probing nor allocation.

if (result.value === void 0) return;

var completion = __conditionalAwait(result.value);

return completion ? Promise.resolve(completion) : result.value;

}

return step((generator = generator.apply(thisArg, _arguments || [])).next());

};__conditionalAwait returns a thenable wrapper or null, with no probe

allocation for plain values. Conditional awaits test this before yielding;

plain values run inline in the generator without involving the runner. Every

actual yield suspends through Promise.resolve, including ordinary awaits.

A standalone await discards its result and needs only a promise temporary;

value-consuming awaits also retain the original value for the plain branch.

Return values are checked for thenables, with an immediate undefined fast

path. The generated ordinaryAsync remains native async at this target and

always returns a Promise, even when its conditional await does not suspend.

This output is standard ES2022 JavaScript. Older supported targets use the

existing async transform where necessary; this example does not promise ES5

support. The P helper parameter is currently unused.

The direct path is selected syntactically for a block containing 1–32 standalone

conditional-await statements, or a block containing one for loop with a

single identifier let initializer and a single standalone conditional await

as its body. Parameters must be simple identifiers without defaults or rest.

Nested awaits/functions/classes, direct eval, arguments, super, and

new.target in the relevant expressions retain the generator fallback, as do

value-consuming awaits, explicit returns, branches, try/finally, and other

loop shapes. Sequences of one to four awaits inline synchronous work and create

callbacks only on suspension branches. Their suffixes are duplicated across

branches, so this optimization is capped at four awaits to bound code growth.

Sequences of five to 32 awaits retain shared continuations; the 32-statement

limit bounds synchronous call depth. A supported loop runs inline initially,

then creates one reusable callback on its first suspension. No type annotation

changes which runtime semantics apply.

For example, this source:

declare function writeChunk(chunk: string): void | Promise<void>;

async? function writeRecord(chunks: string[]): void | Promise<void> {

await? writeChunk(chunks[0]);

await? writeChunk(chunks[1]);

await? writeChunk(chunks[2]);

}async? function writeMany(chunks: string[]): void | Promise<void> {

for (let i = 0; i < chunks.length; i++) await? writeChunk(chunks[i]);

}produces the following ES2022 output (leading "use strict" omitted):

var __conditionalAwait = (this && this.__conditionalAwait) || function (value) {

var then = value !== null && (typeof value === "object" || typeof value === "function") ? value.then : void 0;

return typeof then === "function" ? { then: function (resolve, reject) { Reflect.apply(then, value, [resolve, reject]); } } : null;

};

var __conditionalContinue = (this && this.__conditionalContinue) || function (pending, resume) {

return Promise.resolve(pending).then(resume);

};

function writeRecord(chunks) {

const _pending_1 = __conditionalAwait(writeChunk(chunks[0]));

if (_pending_1)

return __conditionalContinue(_pending_1, () => {

const _pending_4 = __conditionalAwait(writeChunk(chunks[1]));

if (_pending_4)

return __conditionalContinue(_pending_4, () => {

const _pending_6 = __conditionalAwait(writeChunk(chunks[2]));

if (_pending_6)

return __conditionalContinue(_pending_6, () => {

});

});

const _pending_5 = __conditionalAwait(writeChunk(chunks[2]));

if (_pending_5)

return __conditionalContinue(_pending_5, () => {

});

});

const _pending_2 = __conditionalAwait(writeChunk(chunks[1]));

if (_pending_2)

return __conditionalContinue(_pending_2, () => {

const _pending_7 = __conditionalAwait(writeChunk(chunks[2]));

if (_pending_7)

return __conditionalContinue(_pending_7, () => {

});

});

const _pending_3 = __conditionalAwait(writeChunk(chunks[2]));

if (_pending_3)

return __conditionalContinue(_pending_3, () => {

});

}

function writeMany(chunks) {

{

let i = 0;

while (i < chunks.length) {

const _pending_8 = __conditionalAwait(writeChunk(chunks[i]));

if (_pending_8) {

const _resume_1 = () => {

i++;

while (i < chunks.length) {

const _pending_9 = __conditionalAwait(writeChunk(chunks[i]));

if (_pending_9)

return __conditionalContinue(_pending_9, _resume_1);

i++;

}

};

return __conditionalContinue(_pending_8, _resume_1);

}

i++;

}

}

}The empty final callback discards the last thenable's fulfillment value, while

retaining rejection. Each continuation runs only after the preceding write

completes. The short sequence creates no continuation closures when all writes

are plain; each duplicated suffix executes in only one selected branch. The

loop likewise creates no resume callback until it suspends, then reuses that

callback for later flushes. It increments after fulfillment, and synchronous

iterations use while rather than recursive calls. Arrow callbacks retain lexical this;

loop bindings keep an enclosing block scope. Throws before suspension escape

synchronously; throws in a resumed callback reject its Promise.

The __conditionalContinue helper remains inline under --importHelpers and

is collision-safe like the existing helpers. With --noEmitHelpers, consumers

must supply it when direct continuations are emitted. Its global helper scope

also avoids capturing a source parameter named Promise.

Tests cover each suspension position, discarded values, rejections, throwing

then/getters, receiver preservation, deferred then invocation, method this,

loop condition/increment timing, loop variable shadowing, generated-name and

helper collisions, a source parameter named Promise, 100,000 synchronous

iterations without Promise allocation, and fallback for captured loop bindings.

Research checked on 2026-10-09. The sources below include the original

2019 await? discussion, a related committee discussion of mixed returns,

and a newer conditional-iteration discussion. These are distinct proposals.

The 2019 author abandoned the discussion; these sources do not establish a

formal TC39 vote rejecting this exact async? / await? design. A working

compiler fork also does not establish committee acceptance.

- H1: Conditional await, anyone? (October 2019): Gus Caplan, Tab Atkins Jr., Dan Peddle, Isiah Meadows, and Andrea Giammarchi discuss scheduling, use cases, and syntax cost.

- H2: TC39 April 2, 2020 notes, Atomics.waitAsync discussion: delegates discuss predictable Promise contracts, tagged containers, and typing. This is related API-design evidence, not an await?decision.

- H3: for await? .. of (2025–2026): Jordan Harband raises mixed-return concerns; Isaac Schlueter distinguishes callback timing from inspectable completion values. The author later reports a concurrent iterator-request flaw.

- H4: TypeScript Design Goals: upstream favors ECMAScript alignment, preserving JavaScript behavior, and avoiding new expression syntax and runtime functionality.

The historical concerns are summarized below. Mitigations describe this fork's implementation and documentation; the status column deliberately separates implemented protection from accepted or unresolved tradeoffs.

The following are our engineering audit, not claims that every item was raised in the historical threads.

async? function mark(value: number | PromiseLike<number>, log: string[]) {

log.push("before");

await? value;

log.push("after");

}

const plainLog: string[] = [];

const plain = mark(1, plainLog);

plainLog.push("caller");

// plain is undefined; plainLog is ["before", "after", "caller"].

const promisedLog: string[] = [];

const promised = mark(Promise.resolve(1), promisedLog);

promisedLog.push("caller");

// Before completion: ["before", "caller"].

await promised;

// After completion: ["before", "caller", "after"].Where a public API needs a Promise-only result, uniformly deferred body entry, and rejection-only errors, adapt at that boundary:

function writeAllDeferred(chunks: string[]): Promise<void> {

return Promise.resolve().then(() => writeAll(chunks));

}The .then callback delays calling writeAll and captures its synchronous

throws as rejections. Promise.resolve(writeAll(chunks)) would normalize its

return value but would neither defer its body nor catch a synchronous throw.

An ordinary async wrapper also normalizes return/errors, but calling its body

still begins synchronously. Consumers can use standard try/await/catch:

const chunks = ["first", "second"];

try {

await writeAll(chunks);

} catch (error) {

// Handles both a synchronous call failure and a rejected completion.

}Use conditional operations only when both completion paths are part of the

API contract and measurements justify them. None of the above asserts that

changing every ordinary await to await? is behavior-preserving.

Prerequisites: Go 1.27 and Node.js 24.

go build -o ./tsgo ./tsc/cmd/tsc

./tsgo --target es2022 example.ts

TSGO_BINARY="$PWD/tsgo" node --test tools/tests/conditional-async/runtime.test.mjs

go -C tsc test ./internal/parser ./internal/printer ./internal/checker

go -C tsc test ./internal/testrunner -run 'TestLocal/(conditionalAsyncAwait|async.*|await.*|forAwait.*)'The runtime suite compiles and runs five output targets and checks native top-level conditional await. The compiler baseline suite covers syntax, declaration output, inference, annotations, and errors. The existing async baselines are retained unchanged.