[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 (