Skip to content

Sync throw inside async fn body escapes outer try/catch around await #596

Description

@proggeramlug

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 --versionperry 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions