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.