From e68a087399d5f7bc586636c8f39e6c8a7fee0692 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Jul 2026 09:46:15 +0000 Subject: [PATCH 1/2] Initial plan From 93b94455e149aa25d9224f9b5d75098180706e3e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Jul 2026 09:52:43 +0000 Subject: [PATCH 2/2] fix(require-fetch-try-catch): eliminate false positive for await fetch inside directly-awaited async callback Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../src/rules/require-fetch-try-catch.test.ts | 14 ++++++ .../src/rules/require-fetch-try-catch.ts | 46 +++++++++++++++++-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/eslint-factory/src/rules/require-fetch-try-catch.test.ts b/eslint-factory/src/rules/require-fetch-try-catch.test.ts index f33c32e594d..9240663cee5 100644 --- a/eslint-factory/src/rules/require-fetch-try-catch.test.ts +++ b/eslint-factory/src/rules/require-fetch-try-catch.test.ts @@ -125,6 +125,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: [], diff --git a/eslint-factory/src/rules/require-fetch-try-catch.ts b/eslint-factory/src/rules/require-fetch-try-catch.ts index 1d334960fe7..41d12b89973 100644 --- a/eslint-factory/src/rules/require-fetch-try-catch.ts +++ b/eslint-factory/src/rules/require-fetch-try-catch.ts @@ -55,8 +55,12 @@ 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); @@ -64,9 +68,43 @@ export const requireFetchTryCatchRule = createRule({ 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 + ) { + 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; }