main
md 167 lines 5.13 KB
Rendered Raw
1 # validateNoJSXInTryStatement
2
3 ## File
4 `src/Validation/ValidateNoJSXInTryStatement.ts`
5
6 ## Purpose
7 Validates that JSX is not created within a try block. Developers may incorrectly assume that wrapping JSX in try/catch will catch rendering errors, but React does not immediately render components when JSX is created - JSX is just a description of UI that will be rendered later. Error boundaries should be used instead.
8
9 See: https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
10
11 ## Input Invariants
12 - Operates on HIRFunction (pre-reactive scope inference)
13 - Blocks are traversed in order
14 - Only runs when `outputMode === 'lint'`
15
16 ## Validation Rules
17 The pass errors when `JsxExpression` or `JsxFragment` instructions are found within a try block.
18
19 **Error message:**
20 ```
21 Error: Avoid constructing JSX within try/catch
22
23 React does not immediately render components when JSX is rendered, so any errors from this component will not be caught by the try/catch. To catch errors in rendering a given component, wrap that component in an error boundary.
24 ```
25
26 ### Important distinction
27 - JSX in a **try block**: Error
28 - JSX in a **catch block** (not nested in outer try): Allowed
29 - JSX in a **catch block** (nested in outer try): Error
30
31 ## Algorithm
32 1. Maintain a stack `activeTryBlocks` of currently active try statement handler block IDs
33 2. For each block:
34 - Remove the current block from `activeTryBlocks` if it matches a handler (we've exited the try scope)
35 - If `activeTryBlocks` is not empty (we're inside a try block):
36 - Check each instruction for `JsxExpression` or `JsxFragment`
37 - If found, push an error
38 - If the block's terminal is a `try` terminal, push its handler block ID to `activeTryBlocks`
39
40 ### Block tracking with `retainWhere`
41 The `retainWhere` utility is used to remove the current block from `activeTryBlocks` at the start of each block. When we reach a catch handler block, it gets removed from the active list, allowing JSX in catch blocks (unless there's an outer try).
42
43 ## Edge Cases
44
45 ### Allowed: JSX in catch (no outer try)
46 ```javascript
47 // Valid - catch block is not inside a try
48 function Component() {
49 try {
50 doSomething();
51 } catch {
52 return <ErrorMessage />; // OK
53 }
54 }
55 ```
56
57 ### Error: JSX in catch with outer try
58 ```javascript
59 // Error - catch is inside outer try
60 function Component() {
61 try {
62 try {
63 doSomething();
64 } catch {
65 return <ErrorMessage />; // Error!
66 }
67 } catch {
68 return null;
69 }
70 }
71 ```
72
73 ### Error: JSX assigned in try
74 ```javascript
75 // Error - JSX creation is in try block
76 function Component() {
77 let el;
78 try {
79 el = <div />; // Error here
80 } catch {
81 return null;
82 }
83 return el;
84 }
85 ```
86
87 ### Finally blocks
88 The validation currently has TODOs for handling try/catch/finally properly. Files like `error.todo-invalid-jsx-in-try-with-finally.js` indicate these are known unsupported cases.
89
90 ## TODOs
91 Based on fixture naming patterns:
92 - `error.todo-invalid-jsx-in-try-with-finally.js` - Try blocks with finally clauses
93 - `error.todo-invalid-jsx-in-catch-in-outer-try-with-finally.js` - Nested try/catch in try with finally
94
95 ## Example
96
97 ### Fixture: `invalid-jsx-in-try-with-catch.js`
98
99 **Input:**
100 ```javascript
101 // @loggerTestOnly @validateNoJSXInTryStatements @outputMode:"lint"
102 function Component(props) {
103 let el;
104 try {
105 el = <div />;
106 } catch {
107 return null;
108 }
109 return el;
110 }
111 ```
112
113 **Error:**
114 ```
115 Error: Avoid constructing JSX within try/catch
116
117 React does not immediately render components when JSX is rendered, so any errors from this component will not be caught by the try/catch. To catch errors in rendering a given component, wrap that component in an error boundary.
118
119 invalid-jsx-in-try-with-catch.ts:5:9
120 3 | let el;
121 4 | try {
122 > 5 | el = <div />;
123 | ^^^^^^^ Avoid constructing JSX within try/catch
124 6 | } catch {
125 7 | return null;
126 8 | }
127 ```
128
129 **Why it fails:** The `<div />` JSX element is created inside a try block. If the developer expects this to catch errors from rendering the div, they will be surprised - the try/catch will only catch errors from creating the JSX object (which is rare), not from React actually rendering it later. The correct approach is to use an error boundary component to catch rendering errors.
130
131 ### Fixture: `invalid-jsx-in-catch-in-outer-try-with-catch.js`
132
133 **Input:**
134 ```javascript
135 // @loggerTestOnly @validateNoJSXInTryStatements @outputMode:"lint"
136 import {identity} from 'shared-runtime';
137
138 function Component(props) {
139 let el;
140 try {
141 let value;
142 try {
143 value = identity(props.foo);
144 } catch {
145 el = <div value={value} />;
146 }
147 } catch {
148 return null;
149 }
150 return el;
151 }
152 ```
153
154 **Error:**
155 ```
156 Error: Avoid constructing JSX within try/catch
157
158 ...
159
160 invalid-jsx-in-catch-in-outer-try-with-catch.ts:11:11
161 9 | value = identity(props.foo);
162 10 | } catch {
163 > 11 | el = <div value={value} />;
164 | ^^^^^^^^^^^^^^^^^^^^^ Avoid constructing JSX within try/catch
165 ```
166
167 **Why it fails:** Even though the JSX is in a catch block, that catch block is itself inside an outer try block. The outer try's catch won't catch rendering errors from the JSX any more than the inner try would.