Bug
When a try block ends in return or throw, or its catch ends in throw, the CFG never connects an edge into the attached finally block, so every statement in that finally is reported as unreachable_branch at warning severity. Those are exactly the cases finally exists for, so the cleanup is guaranteed to run.
Cause: buildTryStatement (polyscan/internal/js/analyzer/cfg_builder.go:698-712) guards both edges into the finally block with !b.endsWithJump(...):
if tryEndBlock != nil && !b.endsWithJump(tryEndBlock) {
b.cfg.ConnectBlocks(tryEndBlock, finallyBlock, EdgeNormal)
}
if catchBlock != nil && b.currentBlock != nil && !b.endsWithJump(b.currentBlock) {
b.cfg.ConnectBlocks(b.currentBlock, finallyBlock, EdgeNormal)
}
A return or throw inside a try or catch must route through the finally block before leaving the function; the guard drops that edge instead.
Repro
f.ts
export const withReturn = async (t: unknown) => {
let timer: unknown = null;
try {
return await Promise.resolve(t);
} finally {
if (timer) clearCleanup(timer); // line 6
}
};
export const withThrow = (t: unknown) => {
try {
throw new Error('x');
} finally {
cleanup(t); // line 14
}
};
export const noReturnInTry = (t: unknown) => {
try {
cleanup(t);
} finally {
cleanup(t); // line 22
}
};
declare function clearCleanup(x: unknown): void;
declare function cleanup(x: unknown): void;
polyscan analyze --select deadcode --format json f.ts
Actual
withReturn line 6 unreachable_branch This branch is unreachable
withThrow line 14 unreachable_branch This branch is unreachable
Expected
No findings. Both finally bodies run. Line 22 is correctly silent, which isolates the trigger: the false positive appears only when the try (or catch) terminates via return or throw.
Real-world occurrences
On imbue-ai/latchkey@d2ee791, 7 of the 8 unreachable_branch findings are this bug:
| finding |
finally body |
why it is reached |
src/playwrightUtils.ts:601 |
clearTimeout(timer) |
try { return await Promise.race(...) } |
src/playwrightUtils.ts:188-193 |
await browser.close() + temp dir cleanup |
try { ... return result } catch { ... throw error } |
src/services/dropbox.ts:341-342 |
page.off('close', ...) |
same shape |
src/services/fastmail.ts:305-306 |
page.off('close', ...) |
same shape |
src/services/google/base.ts:904-905 |
page.off('close', ...) |
same shape |
src/services/notion-mcp.ts:336-337 |
page.off('close', ...) |
same shape |
src/services/ramp.ts:194-195 |
page.off('close', ...) |
same shape |
All three unreachable_branch findings on almeidazs/better-drizzle@23527ae (src/shared/client/explain.ts:189, src/shared/client/factory.ts:587, :918) are the same: a timer or abort controller released in a finally after return in the try.
Priority
P1 by the audit rubric: a wrong finding on a common construct. try { return } finally { cleanup } is the standard cleanup idiom for cancellable async work, and the findings are warning severity. Good first issue: the fix is local to the two guards above.
polyscan 0.4.1 (commit d41b3bd). Found via the FP-audit skill in repos imbue-ai/latchkey@d2ee7918068c96ccbd12860204502e36575a9d3f and almeidazs/better-drizzle@23527ae.
Bug
When a
tryblock ends inreturnorthrow, or itscatchends inthrow, the CFG never connects an edge into the attachedfinallyblock, so every statement in thatfinallyis reported asunreachable_branchatwarningseverity. Those are exactly the casesfinallyexists for, so the cleanup is guaranteed to run.Cause:
buildTryStatement(polyscan/internal/js/analyzer/cfg_builder.go:698-712) guards both edges into the finally block with!b.endsWithJump(...):A
returnorthrowinside atryorcatchmust route through the finally block before leaving the function; the guard drops that edge instead.Repro
f.tsActual
Expected
No findings. Both
finallybodies run. Line 22 is correctly silent, which isolates the trigger: the false positive appears only when thetry(orcatch) terminates viareturnorthrow.Real-world occurrences
On
imbue-ai/latchkey@d2ee791, 7 of the 8unreachable_branchfindings are this bug:finallybodysrc/playwrightUtils.ts:601clearTimeout(timer)try { return await Promise.race(...) }src/playwrightUtils.ts:188-193await browser.close()+ temp dir cleanuptry { ... return result } catch { ... throw error }src/services/dropbox.ts:341-342page.off('close', ...)src/services/fastmail.ts:305-306page.off('close', ...)src/services/google/base.ts:904-905page.off('close', ...)src/services/notion-mcp.ts:336-337page.off('close', ...)src/services/ramp.ts:194-195page.off('close', ...)All three
unreachable_branchfindings onalmeidazs/better-drizzle@23527ae(src/shared/client/explain.ts:189,src/shared/client/factory.ts:587,:918) are the same: a timer or abort controller released in afinallyafterreturnin thetry.Priority
P1 by the audit rubric: a wrong finding on a common construct.
try { return } finally { cleanup }is the standard cleanup idiom for cancellable async work, and the findings arewarningseverity. Good first issue: the fix is local to the two guards above.polyscan
0.4.1(commitd41b3bd). Found via the FP-audit skill in reposimbue-ai/latchkey@d2ee7918068c96ccbd12860204502e36575a9d3fandalmeidazs/better-drizzle@23527ae.