Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions eslint-factory/src/rules/require-fetch-try-catch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,20 @@ describe("require-fetch-try-catch", () => {
});
});

it("valid: await fetch inside async callback that is itself awaited within an enclosing try", () => {
cjsRuleTester.run("require-fetch-try-catch", requireFetchTryCatchRule, {
valid: [
// Directly-awaited inline arrow callback — rejection propagates to the outer try.
`async function f() { try { config = await withRetry(async () => { const r = await fetch(url); return r; }); } catch (e) {} }`,
// Directly-awaited inline function expression callback.
`async function f() { try { const r = await wrapper(async function() { return await fetch(url); }); } catch (e) {} }`,
// Immediately-invoked async function expression (IIFE) awaited inside try.
`async function f() { try { await (async () => { const r = await fetch(url); })(); } catch (e) {} }`,
],
invalid: [],
});
});

it("invalid: await fetch inside named function declaration nested in outer try block", () => {
cjsRuleTester.run("require-fetch-try-catch", requireFetchTryCatchRule, {
valid: [],
Expand Down
46 changes: 42 additions & 4 deletions eslint-factory/src/rules/require-fetch-try-catch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,18 +112,56 @@ export const requireFetchTryCatchRule = createRule({

/**
* Returns true when node is inside a try block within the same function scope.
* Stops at any function boundary: a try/catch outside the enclosing async function
* cannot catch a rejected promise from an await inside a nested function.
* Stops at any function boundary: a try/catch outside a non-awaited (fire-and-forget)
* callback cannot catch a rejected promise from an await inside that callback.
*
* Exception: if the enclosing function is an inline callback passed to a call expression
* that is itself awaited inside a try block, the rejected promise propagates through the
* awaited chain and IS caught by the outer catch.
*/
function isInsideTryBlock(node: TSESTree.Node): boolean {
const ancestors = sourceCode.getAncestors(node);

for (let i = ancestors.length - 1; i >= 0; i--) {
const ancestor = ancestors[i];

// Any function boundary (declaration, expression, or arrow) stops the search.
// A try/catch outside the current async function cannot protect this await.
// Any function boundary stops the search for non-awaited (fire-and-forget) callbacks.
// Exception: inline FunctionExpression/ArrowFunctionExpression whose parent call is
// itself immediately awaited inside a try block — the rejection propagates up.
if (FUNCTION_BOUNDARY_TYPES.has(ancestor.type)) {
// FunctionDeclarations are never inline callback arguments.
if (ancestor.type === AST_NODE_TYPES.FunctionDeclaration) {
return false;
}
// Check for the directly-awaited inline callback pattern:
// await someWrapper(async () => { await fetch(...) })
// ancestors[i-1] must be the CallExpression, ancestors[i-2] the AwaitExpression.
if (
i >= 2 &&
ancestors[i - 1].type === AST_NODE_TYPES.CallExpression &&
ancestors[i - 2].type === AST_NODE_TYPES.AwaitExpression
Comment on lines +141 to +142
) {
const outerAwait = ancestors[i - 2];
// Now search ancestors outward from i-3 to see if the outer AwaitExpression
// is inside a try block (stopping at the next function boundary).
for (let j = i - 3; j >= 0; j--) {
const outer = ancestors[j];
if (FUNCTION_BOUNDARY_TYPES.has(outer.type)) {
break;
}
if (outer.type === AST_NODE_TYPES.TryStatement && outer.handler != null) {
const block = outer.block;
if (
outerAwait.range != null &&
block.range != null &&
outerAwait.range[0] >= block.range[0] &&
outerAwait.range[1] <= block.range[1]
) {
return true;
}
}
}
}
return false;
}

Expand Down
Loading