Summary
A synchronous throw inside an async (): Promise<T> => { ... } arrow
body — specifically a TypeError raised after an await resolves —
escapes the outer try { await fn(); } catch { … }. The program halts
with the error printed instead of the catch running.
Wrapping the throwing call in another async layer
(async () => { await Promise.resolve(fn()); }) recovers normal
behaviour, suggesting the issue is how Perry promotes a sync throw
post-await into a rejected promise the awaiter can see.
Repro
perry --version → perry 0.5.696
perry compile bug.ts -o bug --no-auto-optimize
./bug
async function direct(): Promise<void> {
let threw = false;
const f = async (): Promise<{ status: number }> => {
return { status: 200 };
};
try {
const r = await f();
const obj = undefined as unknown as { status: number };
const _v = obj.status; // synchronous TypeError, post-await
console.log("direct: should not reach here");
} catch (e) {
threw = true;
console.log("direct caught:", e instanceof Error ? e.message : String(e));
}
console.log(threw ? "direct PASS" : "direct FAIL");
}
async function wrapped(): Promise<void> {
let threw = false;
const f = async (): Promise<{ status: number }> => {
return { status: 200 };
};
const safe = async (): Promise<void> => {
const r = await f();
const obj = undefined as unknown as { status: number };
const _v = obj.status;
};
try {
await safe();
console.log("wrapped: should not reach here");
} catch (e) {
threw = true;
console.log("wrapped caught:", e instanceof Error ? e.message : String(e));
}
console.log(threw ? "wrapped PASS" : "wrapped FAIL");
}
async function main(): Promise<void> {
await direct();
await wrapped();
console.log("END (if we got here, both forms succeeded)");
}
main();
Output
TypeError: Cannot read properties of undefined (reading 'status')
Program exits. No direct caught: / direct FAIL / wrapped caught: /
END output.
Expected
direct caught: Cannot read properties of undefined (reading 'status')
direct PASS
wrapped caught: Cannot read properties of undefined (reading 'status')
wrapped PASS
END (if we got here, both forms succeeded)
Where it bit us
Writing a tiny test runner for our Perry port:
for (const c of cases) {
try {
await Promise.resolve(c.fn()); // c.fn is async, throws sync after await
console.log(`ok ${c.name}`);
} catch (e) {
console.log(`FAIL ${c.name} — ${e.message}`);
}
}
The catch never ran on async tests that threw — instead the runner
exited mid-iteration. Wrapping each call adds a frame that lets the
throw promote to a rejection:
const wrapped = async (): Promise<void> => { await Promise.resolve(c.fn()); };
try { await wrapped(); ... } catch ...
Reliably catches now.
Workaround in user code
Always await inside an extra async wrapper if you need to catch sync
throws that happen post-await inside an async body.
Summary
A synchronous throw inside an
async (): Promise<T> => { ... }arrowbody — specifically a TypeError raised after an
awaitresolves —escapes the outer
try { await fn(); } catch { … }. The program haltswith the error printed instead of the catch running.
Wrapping the throwing call in another async layer
(
async () => { await Promise.resolve(fn()); }) recovers normalbehaviour, suggesting the issue is how Perry promotes a sync throw
post-await into a rejected promise the awaiter can see.
Repro
perry --version→perry 0.5.696Output
Program exits. No
direct caught:/direct FAIL/wrapped caught:/ENDoutput.Expected
Where it bit us
Writing a tiny test runner for our Perry port:
The catch never ran on async tests that threw — instead the runner
exited mid-iteration. Wrapping each call adds a frame that lets the
throw promote to a rejection:
Reliably catches now.
Workaround in user code
Always await inside an extra async wrapper if you need to catch sync
throws that happen post-await inside an async body.