-
-
Notifications
You must be signed in to change notification settings - Fork 185
return, break and continue inside try in async functions don't work correctly #1706
Description
Description
return, break and continue statements inside try blocks in async functions don't preserve TypeScript semantics:
returninsidetry— code after thetry/catchblock still executes (should be unreachable)breakinsidetryin a loop — Lua load-time error:<break> not inside a loopcontinueinsidetryin a loop — Lua load-time error:no visible label
Reproduction: return inside try
let resolveLater!: (value: string) => void;
function deferredPromise(): Promise<string> {
return new Promise(resolve => {
resolveLater = (v) => resolve(v);
});
}
async function fn(): Promise<string> {
try {
return await deferredPromise();
} catch {
return 'caught';
}
print('unreachable!');
}
(async () => {
const promise = fn();
resolveLater('ok');
print(await promise);
})();Expected: print('unreachable!') is dead code — both try and catch branches return.
Actual: print('unreachable!') executes.
Note: the return issue only manifests when the awaited promise resolves asynchronously. With synchronously resolved promises (Promise.resolve()) the bug does not reproduce.
return in a loop (same deferred promise issue — exits only the nested __TS__AsyncAwaiter, loop continues):
async function fn(): Promise<string> {
while (true) {
try {
return await deferredPromise();
} catch {
return 'caught';
}
print('unreachable!');
}
}Reproduction: break and continue inside try in a loop
break:
async function fn(): Promise<void> {
while (true) {
try {
await Promise.resolve();
break;
} catch {}
}
}Expected: breaks out of the loop.
Actual: Lua load-time error: <break> at line ... not inside a loop
continue:
async function fn(): Promise<void> {
for (let i = 0; i < 3; i++) {
try {
await Promise.resolve();
if (i === 1) continue;
} catch {}
print(i);
}
}Expected: prints 0 and 2, skips 1.
Actual: Lua load-time error: no visible label '__continue' for <goto>
Root cause
TSTL compiles try into a nested __TS__AsyncAwaiter (simplified):
local ____try = __TS__AsyncAwaiter(function()
return ____awaiter_resolve(nil, __TS__Await(deferredPromise()))
end)
__TS__Await(____try.catch(____try, function(____)
return ____awaiter_resolve(nil, "caught")
end))
-- code below executes unconditionally after ____try completes
print("unreachable!")return ____awaiter_resolve(...)resolves the outer function's promise but only exits the inner__TS__AsyncAwaitercoroutine — it does not terminate the outer async function- After
__TS__Await(____try.catch(...))the outer function continues executing (resolve + continue) break/continueare emitted inside the inner coroutine function where no enclosing loop or label exists, producing invalid Lua at load time
Workaround
Avoid return, break and continue inside try/catch blocks in async functions. Use variable assignment inside try/catch, then control flow after the block:
// return workaround
async function fn(): Promise<string> {
let result: string;
try {
result = await deferredPromise();
} catch {
result = 'caught';
}
return result;
}
// break workaround
async function fn2(): Promise<void> {
let done = false;
while (!done) {
try {
await doWork();
done = true;
} catch {}
}
}Environment
- TSTL version: 1.34.0
- Lua target: 5.4