@samitouri / QOS-React / commits / b354bbd2d2

[compiler] Update docs with fault tolerance summary, remove planning doc (#35888)

Add concise fault tolerance documentation to CLAUDE.md and the passes README covering error accumulation, tryRecord wrapping, and the distinction between validation vs infrastructure passes. Remove the detailed planning document now that the work is complete.

Joseph Savona committed Feb 23, 2026 at 16:18 UTC b354bbd2d231fdeeec31d438c8e7c54877eee4ac
3 files changed +23 -348
compiler/CLAUDE.md
+14 -15
@@ -229,20 +229,19 @@ Would enable the `enableJsxOutlining` feature and disable the `enableNameAnonymo
229 3. Look for `Impure`, `Render`, `Capture` effects on instructions
230 4. Check the pass ordering in Pipeline.ts to understand when effects are populated vs validated
231
232 -## Error Handling for Unsupported Features
232 +## Error Handling and Fault Tolerance
233
234 -When the compiler encounters an unsupported but known pattern, use `CompilerError.throwTodo()` instead of `CompilerError.invariant()`. Todo errors cause graceful bailouts in production; Invariant errors are hard failures indicating unexpected/invalid states.
234 +The compiler is fault-tolerant: it runs all passes and accumulates errors on the `Environment` rather than throwing on the first error. This lets users see all compilation errors at once.
235
236 -```typescript
237 -// Unsupported but expected pattern - graceful bailout
238 -CompilerError.throwTodo({
239 - reason: `Support [description of unsupported feature]`,
240 - loc: terminal.loc,
241 -});
242 -
243 -// Invariant is for truly unexpected/invalid states - hard failure
244 -CompilerError.invariant(false, {
245 - reason: `Unexpected [thing]`,
246 - loc: terminal.loc,
247 -});
248 -```
236 +**Recording errors** — Passes record errors via `env.recordError(diagnostic)`. Errors are accumulated on `Environment.#errors` and checked at the end of the pipeline via `env.hasErrors()` / `env.aggregateErrors()`.
237 +
238 +**`tryRecord()` wrapper** — In Pipeline.ts, validation passes are wrapped in `env.tryRecord(() => pass(hir))` which catches thrown `CompilerError`s (non-invariant) and records them. Infrastructure/transformation passes are NOT wrapped in `tryRecord()` because later passes depend on their output being structurally valid.
239 +
240 +**Error categories:**
241 +- `CompilerError.throwTodo()` — Unsupported but known pattern. Graceful bailout. Can be caught by `tryRecord()`.
242 +- `CompilerError.invariant()` — Truly unexpected/invalid state. Always throws immediately, never caught by `tryRecord()`.
243 +- Non-`CompilerError` exceptions — Always re-thrown.
244 +
245 +**Key files:** `Environment.ts` (`recordError`, `tryRecord`, `hasErrors`, `aggregateErrors`), `Pipeline.ts` (pass orchestration), `Program.ts` (`tryCompileFunction` handles the `Result`).
246 +
247 +**Test fixtures:** `__tests__/fixtures/compiler/fault-tolerance/` contains multi-error fixtures verifying all errors are reported.
compiler/fault-tolerance-overview.md deleted
-333
@@ -1,333 +0,0 @@
1 -## React Compiler Fault Tolerance
2 -
3 -Update React Compiler (@compiler/ directory) to always run all passes and return either the transformed code (if no error) or a list of one or more compilation errors.
4 -
5 -## Background
6 -
7 -Currently React Compiler runs through a series of passes in Pipeline.ts. If an error occurs in a pass the compiler will generally either throw the error in the pass where it occurs, or return a Result<_, CompilerError> which is then unwrapped in Pipeline.ts, throwing there. This means that a single error that triggers early can prevent later validation from running, meaning the user has to first fix one error in order to see another.
8 -
9 -## New Approach
10 -
11 -The compiler should always run all passes in the pipeline, up to and including CodegenReactiveFunction. During this process it should accumulate errors. If at the end of compilation there were no accumulated errors, return `Ok(generatedfunction)`. Else, return `Err(CompilerError)` with *all* the accumulated errors.
12 -
13 -Note that some errors may continue to cause an eager bailout:
14 -* If an error is not an instanceof CompilerError, throw it as it occurs
15 -* If an error is a CompilerError invariant, throw it as it occurs since this represents a truly exceptional, unexpected case
16 -
17 -## Detailed Design
18 -
19 -* The Environment needs a way to record errors as compilation proceeds. This should generally store the error (and log, if a logger is configured), but should immediately throw if the error is an invariant (see above).
20 -* BuildHIR should always produce an HIR without error. For syntax forms that are unsupported (currently throwing a Todo error), we should instead construct record the todo error on the environment, and construct a partial HIR. The exact form of the partial HIR can be situation specific:
21 - * `var` is currently unsupported, but we could pretend it was `let`
22 - * `finally` blocks are unsupported, we could just prune them, or move the code after the try/catch (put the finally logic in the consequent)
23 - * This may mean updating the HIR to allow representing partial code
24 - * `eval()` can just be an Unsupported InstructionValue variant
25 -* All of the passes need to be updated to stop returning Result or CompilerError, and instead record their errors on the environment. They should always be able to proceed even in the presence of errors. For example, in InferMutationAliasingEffects if we discover that the code mutates a frozen value, we can record this as an error and then just pretend the mutation didn't happen - ie construct a scope as if the mutating code was not a mutation after all.
26 -* Finally, the end of the pipeline should check for errors and either turn `Ok(GeneratedFunction)` or `Err(aggregatedErrors)`. The code calling into the pipeline then needs to handle this appropriately.
27 -
28 -## Detailed Plan
29 -
30 -### Phase 1: Environment Error Accumulation Infrastructure
31 -
32 -Add error accumulation to the `Environment` class so that any pass can record errors during compilation without halting.
33 -
34 -- [x] **1.1 Add error accumulator to Environment** (`src/HIR/Environment.ts`)
35 - - Add a `#errors: CompilerError` field, initialized in the constructor
36 - - Add a `recordError(error: CompilerDiagnostic | CompilerErrorDetail)` method that:
37 - - If an Invariant-category detail, immediately throw it
38 - - Otherwise, push the diagnostic/detail onto `#errors` (and log via `this.logger` if configured)
39 - - Add a `recordErrors(error: CompilerError)` method that calls `recordError()` for each of the details on the given error.
40 - - Add a `hasErrors(): boolean` getter
41 - - Add a `aggregateErrors(): CompilerError` method that returns the accumulated error object
42 - - Consider whether `recordError` should accept the same options as `CompilerError.push()` for convenience (reason, description, severity, loc, etc.)
43 -
44 -- [x] **1.2 Add a `tryRecord` helper on Environment** (`src/HIR/Environment.ts`)
45 - - Add a `tryRecord(fn: () => void): void` method that wraps a callback in try/catch:
46 - - If `fn` throws a `CompilerError` that is NOT an invariant, record it via `recordError`
47 - - If `fn` throws a non-CompilerError or a CompilerError invariant, re-throw
48 - - This helper is the migration path for passes that currently throw: wrap their call in `env.tryRecord(() => pass(hir))` so exceptions become recorded errors
49 -
50 -### Phase 2: Update Pipeline.ts to Accumulate Errors
51 -
52 -Change `runWithEnvironment` to run all passes and check for errors at the end instead of letting exceptions propagate.
53 -
54 -- [x] **2.1 Change `runWithEnvironment` return type** (`src/Entrypoint/Pipeline.ts`)
55 - - Change return type from `CodegenFunction` to `Result<CodegenFunction, CompilerError>`
56 - - At the end of the pipeline, check `env.hasErrors()`:
57 - - If no errors: return `Ok(ast)`
58 - - If errors: return `Err(env.aggregateErrors())`
59 -
60 -- [x] **2.2 Update `compileFn` to propagate the Result** (`src/Entrypoint/Pipeline.ts`)
61 - - Change `compileFn` return type from `CodegenFunction` to `Result<CodegenFunction, CompilerError>`
62 - - Propagate the Result from `runWithEnvironment`
63 -
64 -- [x] **2.3 Update `run` to propagate the Result** (`src/Entrypoint/Pipeline.ts`)
65 - - Same change for the internal `run` function
66 -
67 -- [x] **2.4 Update callers in Program.ts** (`src/Entrypoint/Program.ts`)
68 - - In `tryCompileFunction`, change from try/catch around `compileFn` to handling the `Result`:
69 - - If `Ok(codegenFn)`: return the compiled function
70 - - If `Err(compilerError)`: return `{kind: 'error', error: compilerError}`
71 - - Keep the try/catch only for truly unexpected (non-CompilerError) exceptions and invariants
72 - - The existing `handleError`/`logError`/`panicThreshold` logic in `processFn` should continue to work unchanged since it already handles `CompilerError` instances
73 -
74 -### Phase 3: Update BuildHIR (lower) to Always Produce HIR
75 -
76 -Currently `lower()` returns `Result<HIRFunction, CompilerError>`. It already accumulates errors internally via `builder.errors`, but returns `Err` when errors exist. Change it to always return `Ok(hir)` while recording errors on the environment.
77 -
78 -- [x] **3.1 Change `lower` to always return HIRFunction** (`src/HIR/BuildHIR.ts`)
79 - - Change return type from `Result<HIRFunction, CompilerError>` to `HIRFunction`
80 - - Instead of returning `Err(builder.errors)` at line 227-229, record errors on `env` via `env.recordErrors(builder.errors)` and return the (partial) HIR
81 - - Update the pipeline to call `lower(func, env)` directly instead of `lower(func, env).unwrap()`
82 - - Added try/catch around body lowering to catch thrown CompilerErrors (e.g., from `resolveBinding`) and record them
83 -
84 -- [x] **3.2 Handle `var` declarations as `let`** (`src/HIR/BuildHIR.ts`, line ~855)
85 - - Record the Todo error, then treat `var` as `let` and continue lowering (instead of skipping the declaration)
86 -
87 -- [x] **3.3 Handle `try/finally` by pruning `finally`** (`src/HIR/BuildHIR.ts`, lines ~1281-1296)
88 - - Already handled: `try` without `catch` pushes error and returns; `try` with `finally` pushes error and continues with `try/catch` portion only
89 -
90 -- [x] **3.4 Handle `eval()` via UnsupportedNode** (`src/HIR/BuildHIR.ts`, line ~3568)
91 - - Already handled: records error via `builder.errors.push()` and continues
92 -
93 -- [x] **3.5 Handle `with` statement via UnsupportedNode** (`src/HIR/BuildHIR.ts`, line ~1382)
94 - - Already handled: records error and emits `UnsupportedNode`
95 -
96 -- [x] **3.6 Handle inline `class` declarations** (`src/HIR/BuildHIR.ts`, line ~1402)
97 - - Already handled: records error and emits `UnsupportedNode`
98 -
99 -- [x] **3.7 Handle remaining Todo errors in expression lowering** (`src/HIR/BuildHIR.ts`)
100 - - Already handled: all ~60 error sites use `builder.errors.push()` to accumulate errors. The try/catch around body lowering provides a safety net for any that still throw.
101 -
102 -- [x] **3.8 Handle `throw` inside `try/catch`** (`src/HIR/BuildHIR.ts`, line ~284)
103 - - Already handled: records error via `builder.errors.push()` and continues
104 -
105 -- [x] **3.9 Handle `for` loops with missing test or expression init** (`src/HIR/BuildHIR.ts`, lines ~559, ~632)
106 - - For `for(;;)` (missing test): emit `true` as the test expression and add a branch terminal
107 - - For empty init (`for (; ...)`): add a placeholder instruction to avoid invariant about empty blocks
108 - - For expression init (`for (expr; ...)`): record error and lower the expression as best-effort
109 - - Changed `'unsupported'` terminal to `'goto'` terminal for non-variable init to maintain valid CFG structure
110 -
111 -- [x] **3.10 Handle nested function lowering failures** (`src/HIR/BuildHIR.ts`, `lowerFunction` at line ~3504)
112 - - `lowerFunction()` now always returns `LoweredFunction` since `lower()` always returns `HIRFunction`
113 - - Errors from nested functions are recorded on the shared environment
114 - - Removed the `null` return case and the corresponding `UnsupportedNode` fallback in callers
115 -
116 -- [x] **3.11 Handle unreachable functions in `build()`** (`src/HIR/HIRBuilder.ts`, `build()`)
117 - - Changed `CompilerError.throwTodo()` for unreachable code with hoisted declarations to `this.errors.push()` to allow HIR construction to complete
118 -
119 -- [x] **3.12 Handle duplicate fbt tags** (`src/HIR/BuildHIR.ts`, line ~2279)
120 - - Changed `CompilerError.throwDiagnostic()` to `builder.errors.pushDiagnostic()` to record instead of throw
121 -
122 -### Phase 4: Update Validation Passes
123 -
124 -All validation passes need to record errors on the environment instead of returning `Result` or throwing. They should still detect the same problems, but the pipeline should continue after each one.
125 -
126 -#### Pattern A passes (currently return `Result`, called with `.unwrap()`)
127 -
128 -These passes already accumulate errors internally and return `Result<void, CompilerError>`. The change is: instead of returning the Result, record errors on `env` and return void. Remove the `.unwrap()` call in Pipeline.ts.
129 -
130 -- [x] **4.1 `validateHooksUsage`** (`src/Validation/ValidateHooksUsage.ts`)
131 - - Change signature from `(fn: HIRFunction): Result<void, CompilerError>` to `(fn: HIRFunction): void`
132 - - Record errors on `fn.env` instead of returning `errors.asResult()`
133 - - Update Pipeline.ts call site (line 211): remove `.unwrap()`
134 -
135 -- [x] **4.2 `validateNoCapitalizedCalls`** (`src/Validation/ValidateNoCapitalizedCalls.ts`)
136 - - Change signature to return void
137 - - Fix the hybrid pattern: the direct `CallExpression` path currently throws via `CompilerError.throwInvalidReact()` — change to record on env
138 - - The `MethodCall` path already accumulates — change to record on env
139 - - Update Pipeline.ts call site (line 214): remove `.unwrap()`
140 -
141 -- [x] **4.3 `validateUseMemo`** (`src/Validation/ValidateUseMemo.ts`)
142 - - Change signature to return void
143 - - Record hard errors on env instead of returning `errors.asResult()`
144 - - The soft `voidMemoErrors` path already uses `env.logErrors()` — keep as-is or also record
145 - - Update Pipeline.ts call site (line 170): remove `.unwrap()`
146 -
147 -- [x] **4.4 `dropManualMemoization`** (`src/Inference/DropManualMemoization.ts`)
148 - - Change signature to return void
149 - - Record errors on env instead of returning `errors.asResult()`
150 - - Update Pipeline.ts call site (line 178): remove `.unwrap()`
151 -
152 -- [x] **4.5 `validateNoRefAccessInRender`** (`src/Validation/ValidateNoRefAccessInRender.ts`)
153 - - Change signature to return void
154 - - Record errors on env instead of returning Result
155 - - Update Pipeline.ts call site (line 275): remove `.unwrap()`
156 -
157 -- [x] **4.6 `validateNoSetStateInRender`** (`src/Validation/ValidateNoSetStateInRender.ts`)
158 - - Change signature to return void
159 - - Record errors on env
160 - - Update Pipeline.ts call site (line 279): remove `.unwrap()`
161 -
162 -- [x] **4.7 `validateNoImpureFunctionsInRender`** (`src/Validation/ValidateNoImpureFunctionsInRender.ts`)
163 - - Change signature to return void
164 - - Record errors on env
165 - - Update Pipeline.ts call site (line 300): remove `.unwrap()`
166 -
167 -- [x] **4.8 `validateNoFreezingKnownMutableFunctions`** (`src/Validation/ValidateNoFreezingKnownMutableFunctions.ts`)
168 - - Change signature to return void
169 - - Record errors on env
170 - - Update Pipeline.ts call site (line 303): remove `.unwrap()`
171 -
172 -- [x] **4.9 `validateExhaustiveDependencies`** (`src/Validation/ValidateExhaustiveDependencies.ts`)
173 - - Change signature to return void
174 - - Record errors on env
175 - - Update Pipeline.ts call site (line 315): remove `.unwrap()`
176 -
177 -- [x] **4.10 `validateMemoizedEffectDependencies`** (`src/Validation/ValidateMemoizedEffectDependencies.ts`)
178 - - Change signature to return void (note: operates on `ReactiveFunction`)
179 - - Record errors on the function's env
180 - - Update Pipeline.ts call site (line 565): remove `.unwrap()`
181 -
182 -- [x] **4.11 `validatePreservedManualMemoization`** (`src/Validation/ValidatePreservedManualMemoization.ts`)
183 - - Change signature to return void (note: operates on `ReactiveFunction`)
184 - - Record errors on the function's env
185 - - Update Pipeline.ts call site (line 572): remove `.unwrap()`
186 -
187 -- [x] **4.12 `validateSourceLocations`** (`src/Validation/ValidateSourceLocations.ts`)
188 - - Change signature to return void
189 - - Record errors on env
190 - - Update Pipeline.ts call site (line 585): remove `.unwrap()`
191 -
192 -#### Pattern B passes (currently use `env.logErrors()`)
193 -
194 -These already use a soft-logging pattern and don't block compilation. They can be migrated to `env.recordError()` so all errors are aggregated in one place.
195 -
196 -- [ ] **4.13 `validateNoDerivedComputationsInEffects_exp`** — change to record on env directly
197 -- [ ] **4.14 `validateNoSetStateInEffects`** — change to record on env directly
198 -- [ ] **4.15 `validateNoJSXInTryStatement`** — change to record on env directly
199 -- [ ] **4.16 `validateStaticComponents`** — change to record on env directly
200 -
201 -#### Pattern D passes (currently throw directly, no Result)
202 -
203 -These throw `CompilerError` directly (not via Result). They need the most work.
204 -
205 -- [x] **4.17 `validateContextVariableLValues`** (`src/Validation/ValidateContextVariableLValues.ts`)
206 - - Currently throws via `CompilerError.throwTodo()` and `CompilerError.invariant()`
207 - - Change to record Todo errors on env and continue
208 - - Keep invariant throws (those indicate internal bugs)
209 -
210 -- [x] **4.18 `validateLocalsNotReassignedAfterRender`** (`src/Validation/ValidateLocalsNotReassignedAfterRender.ts`)
211 - - Currently constructs a `CompilerError` and `throw`s it directly
212 - - Change to record errors on env
213 -
214 -- [x] **4.19 `validateNoDerivedComputationsInEffects`** (`src/Validation/ValidateNoDerivedComputationsInEffects.ts`)
215 - - Currently throws directly
216 - - Change to record errors on env
217 -
218 -### Phase 5: Update Inference Passes
219 -
220 -The inference passes are the most critical to handle correctly because they produce side effects (populating effects on instructions, computing mutable ranges) that downstream passes depend on. They must continue producing valid (even if imprecise) output when errors are encountered.
221 -
222 -- [x] **5.1 `inferMutationAliasingEffects`** (`src/Inference/InferMutationAliasingEffects.ts`)
223 - - Currently returns `Result<void, CompilerError>` — errors are about mutation of frozen/global values
224 - - Change to record errors on `fn.env` instead of accumulating internally
225 - - **Key recovery strategy**: When a mutation of a frozen value is detected, record the error but treat the operation as a non-mutating read. This way downstream passes see a consistent (if conservative) view
226 - - When a mutation of a global is detected, record the error but continue with the global unchanged
227 - - Update Pipeline.ts (lines 233-239): remove the conditional `.isErr()` / throw pattern
228 -
229 -- [x] **5.2 `inferMutationAliasingRanges`** (`src/Inference/InferMutationAliasingRanges.ts`)
230 - - Currently returns `Result<Array<AliasingEffect>, CompilerError>`
231 - - This pass has a meaningful success value (the function's external aliasing effects)
232 - - Change to: always produce a best-effort effects array, record errors on env
233 - - When errors are encountered, produce conservative effects (e.g., assume no external mutation)
234 - - Update Pipeline.ts (lines 258-267): remove the conditional throw pattern, call directly
235 -
236 -### Phase 6: Update Codegen
237 -
238 -- [x] **6.1 `codegenFunction`** (`src/ReactiveScopes/CodegenReactiveFunction.ts`)
239 - - Currently returns `Result<CodegenFunction, CompilerError>`
240 - - Change to: always produce a `CodegenFunction`, record errors on env
241 - - If codegen encounters an error (e.g., an instruction it can't generate code for), it should:
242 - - Record the error
243 - - For `UnsupportedNode` values: pass through the original AST node (already works this way)
244 - - For other error cases: emit a placeholder or the original AST where possible
245 - - Update Pipeline.ts (line 575-578): remove `.unwrap()`
246 -
247 -### Phase 7: Pipeline.ts Pass-by-Pass Migration
248 -
249 -Walk through `runWithEnvironment` and wrap each pass call site. This is the integration work tying Phases 3-6 together.
250 -
251 -- [x] **7.1 Wrap `lower()` call** (line 163)
252 - - Change from `lower(func, env).unwrap()` to `lower(func, env)` (direct return after Phase 3.1)
253 -
254 -- [x] **7.2 Wrap validation calls that use `.unwrap()`** (lines 169-303)
255 - - Remove `.unwrap()` from all validation calls after they're updated in Phase 4
256 - - For validations guarded by `env.enableValidations`, keep the guard but remove the `.unwrap()`
257 -
258 -- [x] **7.3 Wrap inference calls** (lines 233-267)
259 - - After Phase 5, `inferMutationAliasingEffects` and `inferMutationAliasingRanges` record errors directly
260 - - Remove the `mutabilityAliasingErrors` / `mutabilityAliasingRangeErrors` variables and their conditional throw logic
261 -
262 -- [x] **7.4 Wrap `env.logErrors()` calls** (lines 286-331)
263 - - After Phase 4.13-4.16, these passes record on env directly
264 - - Remove the `env.logErrors()` wrapper calls
265 -
266 -- [x] **7.5 Wrap codegen** (lines 575-578)
267 - - After Phase 6.1, `codegenFunction` returns directly
268 - - Remove the `.unwrap()`
269 -
270 -- [x] **7.6 Add final error check** (end of `runWithEnvironment`)
271 - - After all passes complete, check `env.hasErrors()`
272 - - If no errors: return `Ok(ast)`
273 - - If errors: return `Err(env.aggregateErrors())`
274 -
275 -- [x] **7.7 Consider wrapping each pass in `env.tryRecord()`** as a safety net
276 - - Even after individual passes are updated, wrapping each pass call in `env.tryRecord()` provides defense-in-depth
277 - - If a pass unexpectedly throws a CompilerError (e.g., from a code path we missed), it gets caught and recorded rather than aborting the pipeline
278 - - Non-CompilerError exceptions and invariants still propagate immediately
279 -
280 -### Phase 8: Testing
281 -
282 -- [x] **8.1 Update existing `error.todo-*` fixture expectations**
283 - - Currently, fixtures with `error.todo-` prefix expect a single error and bailout
284 - - After fault tolerance, some of these may now produce multiple errors
285 - - Update the `.expect.md` files to reflect the new aggregated error output
286 -
287 -- [x] **8.2 Add multi-error test fixtures**
288 - - Create test fixtures that contain multiple independent errors (e.g., both a `var` declaration and a mutation of a frozen value)
289 - - Verify that all errors are reported, not just the first one
290 -
291 -- [x] **8.3 Add test for invariant-still-throws behavior**
292 - - Verify that `CompilerError.invariant()` failures still cause immediate abort
293 - - Verify that non-CompilerError exceptions still cause immediate abort
294 -
295 -- [x] **8.4 Add test for partial HIR codegen**
296 - - Verify that when BuildHIR produces partial HIR (with `UnsupportedNode` values), later passes handle it gracefully and codegen produces the original AST for unsupported portions
297 -
298 -- [x] **8.5 Verify error severity in aggregated output**
299 - - Test that the aggregated `CompilerError` correctly reports `hasErrors()` vs `hasWarning()` vs `hasHints()` based on the mix of accumulated diagnostics
300 - - Verify that `panicThreshold` behavior in Program.ts is correct for aggregated errors
301 -
302 -- [x] **8.6 Run full test suite**
303 - - Run `yarn snap` and `yarn snap -u` to update all fixture expectations
304 - - Ensure no regressions in passing tests
305 -
306 -### Implementation Notes
307 -
308 -**Ordering**: Phases 1 → 2 → 3 → 4/5/6 (parallel) → 7 → 8. Phase 1 (Environment infrastructure) is the foundation. Phase 2 (Pipeline return type) sets up the contract. Phases 3-6 can be done incrementally — each pass can be migrated independently using `env.tryRecord()` as a transitional wrapper. Phase 7 is the integration. Phase 8 validates everything.
309 -
310 -**Incremental migration path**: Rather than updating all passes at once, each pass can be individually migrated. During the transition:
311 -1. First add `env.tryRecord()` in Phase 7.7 around all pass calls in the pipeline — this immediately provides fault tolerance by catching any thrown CompilerError
312 -2. Then individually update passes (Phases 3-6) to record errors directly on env, which is cleaner but not required for the basic behavior
313 -3. This means the feature can be landed incrementally: Phase 1 + 2 + 7.7 gives basic fault tolerance, then individual passes can be refined over time
314 -
315 -**What NOT to change**:
316 -- `CompilerError.invariant()` must continue to throw immediately — these represent internal bugs
317 -- Non-CompilerError exceptions must continue to throw — these are unexpected JS errors
318 -- The `assertConsistentIdentifiers`, `assertTerminalSuccessorsExist`, `assertTerminalPredsExist`, `assertValidBlockNesting`, `assertValidMutableRanges`, `assertWellFormedBreakTargets`, `assertScopeInstructionsWithinScopes` assertion functions should continue to throw — they are invariant checks on internal data structure consistency
319 -- The `panicThreshold` mechanism in Program.ts should continue to work — it now operates on the aggregated error from the Result rather than a caught exception, but the behavior is the same
320 -
321 -## Key Learnings
322 -
323 -* **Phase 2+7 (Pipeline tryRecord wrapping) was sufficient for basic fault tolerance.** Wrapping all passes in `env.tryRecord()` immediately enabled the compiler to continue past errors that previously threw. This caused 52 test fixtures to produce additional errors that were previously masked by the first error bailing out. For example, `error.todo-reassign-const` previously reported only "Support destructuring of context variables" but now also reports the immutability violation.
324 -* **Lint-only passes (Pattern B: `env.logErrors()`) should not use `tryRecord()`/`recordError()`** because those errors are intentionally non-blocking. They are reported via the logger only and should not cause the pipeline to return `Err`. The `logErrors` pattern was kept for `validateNoDerivedComputationsInEffects_exp`, `validateNoSetStateInEffects`, `validateNoJSXInTryStatement`, and `validateStaticComponents`.
325 -* **Inference passes that return `Result` with validation errors** (`inferMutationAliasingEffects`, `inferMutationAliasingRanges`) were changed to record errors via `env.recordErrors()` instead of throwing, allowing subsequent passes to proceed.
326 -* **Value-producing passes** (`memoizeFbtAndMacroOperandsInSameScope`, `renameVariables`, `buildReactiveFunction`) need safe default values when wrapped in `tryRecord()` since the callback can't return values. We initialize with empty defaults (e.g., `new Set()`) before the `tryRecord()` call.
327 -* **Phase 3 (BuildHIR) revealed that most error sites already used `builder.errors.push()` for accumulation.** The existing lowering code was designed to accumulate errors rather than throw. The main changes were: (1) changing `lower()` return type from `Result` to `HIRFunction`, (2) recording builder errors on env, (3) adding a try/catch around body lowering to catch thrown CompilerErrors from sub-calls like `resolveBinding()`, (4) treating `var` as `let` instead of skipping declarations, and (5) fixing ForStatement init/test handling to produce valid CFG structure.
328 -* **Partial HIR can trigger downstream invariants.** When lowering skips or partially handles constructs (e.g., unreachable hoisted functions, `var` declarations before the fix), downstream passes like `InferMutationAliasingEffects` may encounter uninitialized identifiers and throw invariants. This is acceptable since the function still correctly bails out of compilation, but error messages may be less specific. The fix for `var` (treating as `let`) demonstrates how to avoid this: continue lowering with a best-effort representation rather than skipping entirely.
329 -* **Errors accumulated on `env` are lost when an invariant propagates out of the pipeline.** Since invariant CompilerErrors always re-throw through `tryRecord()`, they exit the pipeline as exceptions. The caller only sees the invariant error, not any errors previously recorded on `env`. This is a design limitation that could be addressed by aggregating env errors with caught exceptions in `tryCompileFunction()`.
330 -* **Dedicated fault tolerance test fixtures** were added in `__tests__/fixtures/compiler/fault-tolerance/`. Each fixture combines two or more errors from different passes to verify the compiler reports all of them rather than short-circuiting on the first. Coverage includes: `var`+props mutation (BuildHIR→InferMutationAliasingEffects), `var`+ref access (BuildHIR→ValidateNoRefAccessInRender), `try/finally`+props mutation (BuildHIR→InferMutationAliasingEffects), `try/finally`+ref access (BuildHIR→ValidateNoRefAccessInRender), and a 3-error test combining try/finally+ref access+props mutation.
331 -* **Cleanup: consistent `tryRecord()` wrapping in Pipeline.ts.** All validation passes and inference passes are now wrapped in `env.tryRecord()` for defense-in-depth, consistent with the approach used for transform passes. Previously only transform passes were wrapped. Merged duplicate `env.enableValidations` guard blocks. Pattern B lint-only passes (`env.logErrors()`) were intentionally not wrapped since they use a different error recording strategy.
332 -* **Cleanup: normalized validation error recording pattern.** Four validation passes (`ValidateNoDerivedComputationsInEffects`, `ValidateMemoizedEffectDependencies`, `ValidatePreservedManualMemoization`, `ValidateSourceLocations`) were using `for (const detail of errors.details) { env.recordError(detail); }` instead of the simpler `env.recordErrors(errors)`. Normalized to use the batch method.
333 -
compiler/packages/babel-plugin-react-compiler/docs/passes/README.md
+9
@@ -302,6 +302,15 @@ yarn snap minimize <path>
302 yarn snap -u
303 ```
304
305 +## Fault Tolerance
306 +
307 +The pipeline is fault-tolerant: all passes run to completion, accumulating errors on `Environment` rather than aborting on the first error.
308 +
309 +- **Validation passes** are wrapped in `env.tryRecord()` in Pipeline.ts, which catches non-invariant `CompilerError`s and records them. If a validation pass throws, compilation continues.
310 +- **Infrastructure/transformation passes** (enterSSA, eliminateRedundantPhi, inferMutationAliasingEffects, codegen, etc.) are NOT wrapped in `tryRecord()` because subsequent passes depend on their output being structurally valid. If they fail, compilation aborts.
311 +- **`lower()` (BuildHIR)** always produces an `HIRFunction`, recording errors on `env` instead of returning `Err`. Unsupported constructs (e.g., `var`) are lowered best-effort.
312 +- At the end of the pipeline, `env.hasErrors()` determines whether to return `Ok(codegen)` or `Err(aggregatedErrors)`.
313 +
314 ## Further Reading
315
316 - [MUTABILITY_ALIASING_MODEL.md](../../src/Inference/MUTABILITY_ALIASING_MODEL.md): Detailed aliasing model docs