@samitouri / QOS-React / commits / 820af20971

[eslint] Disallow use within try/catch blocks (#34040)

Follow up to #34032. The linter now ensures that `use` cannot be used within try/catch.

lauren committed Jul 29, 2025 at 12:33 UTC 820af2097103309fdc5675d2bde744103a439eff
2 files changed +65 -3
packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js
+36 -2
@@ -1324,6 +1324,34 @@ const allTests = {
1324 `,
1325 errors: [asyncComponentHookError('use')],
1326 },
1327 + {
1328 + code: normalizeIndent`
1329 + function App({p1, p2}) {
1330 + try {
1331 + use(p1);
1332 + } catch (error) {
1333 + console.error(error);
1334 + }
1335 + use(p2);
1336 + return <div>App</div>;
1337 + }
1338 + `,
1339 + errors: [tryCatchUseError('use')],
1340 + },
1341 + {
1342 + code: normalizeIndent`
1343 + function App({p1, p2}) {
1344 + try {
1345 + doSomething();
1346 + } catch {
1347 + use(p1);
1348 + }
1349 + use(p2);
1350 + return <div>App</div>;
1351 + }
1352 + `,
1353 + errors: [tryCatchUseError('use')],
1354 + },
1355 ],
1356 };
1357
@@ -1383,7 +1411,7 @@ if (__EXPERIMENTAL__) {
1411 const onEvent = useEffectEvent((text) => {
1412 console.log(text);
1413 });
1386 -
1414 +
1415 useEffect(() => {
1416 onEvent('Hello world');
1417 });
@@ -1421,7 +1449,7 @@ if (__EXPERIMENTAL__) {
1449 });
1450 return <Child onClick={() => onClick()} />
1451 }
1424 -
1452 +
1453 // The useEffectEvent function shares an identifier name with the above
1454 function MyLastComponent({theme}) {
1455 const onClick = useEffectEvent(() => {
@@ -1573,6 +1601,12 @@ function asyncComponentHookError(fn) {
1601 };
1602 }
1603
1604 +function tryCatchUseError(fn) {
1605 + return {
1606 + message: `React Hook "${fn}" cannot be called in a try/catch block.`,
1607 + };
1608 +}
1609 +
1610 // For easier local testing
1611 if (!process.env.CI) {
1612 let only = [];
packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts
+29 -1
@@ -7,7 +7,13 @@
7 /* eslint-disable no-for-of-loops/no-for-of-loops */
8
9 import type {Rule, Scope} from 'eslint';
10 -import type {CallExpression, DoWhileStatement, Node} from 'estree';
10 +import type {
11 + CallExpression,
12 + CatchClause,
13 + DoWhileStatement,
14 + Node,
15 + TryStatement,
16 +} from 'estree';
17
18 // @ts-expect-error untyped module
19 import CodePathAnalyzer from '../code-path-analysis/code-path-analyzer';
@@ -111,6 +117,18 @@ function isInsideDoWhileLoop(node: Node | undefined): node is DoWhileStatement {
117 return false;
118 }
119
120 +function isInsideTryCatch(
121 + node: Node | undefined,
122 +): node is TryStatement | CatchClause {
123 + while (node) {
124 + if (node.type === 'TryStatement' || node.type === 'CatchClause') {
125 + return true;
126 + }
127 + node = node.parent;
128 + }
129 + return false;
130 +}
131 +
132 function isUseEffectEventIdentifier(node: Node): boolean {
133 if (__EXPERIMENTAL__) {
134 return node.type === 'Identifier' && node.name === 'useEffectEvent';
@@ -532,6 +550,16 @@ const rule = {
550 continue;
551 }
552
553 + // Report an error if use() is called inside try/catch.
554 + if (isUseIdentifier(hook) && isInsideTryCatch(hook)) {
555 + context.report({
556 + node: hook,
557 + message: `React Hook "${getSourceCode().getText(
558 + hook,
559 + )}" cannot be called in a try/catch block.`,
560 + });
561 + }
562 +
563 // Report an error if a hook may be called more then once.
564 // `use(...)` can be called in loops.
565 if (