Repository: facebook/react
Pull request: [compiler] Phase 1: Add error accumulation infrastructure to Environment


Add error accumulation methods to the Environment class:
- #errors field to accumulate CompilerErrors across passes
- recordError() to record a single diagnostic (throws if Invariant)
- recordErrors() to record all diagnostics from a CompilerError
- hasErrors() to check if any errors have been recorded
- aggregateErrors() to retrieve the accumulated CompilerError
- tryRecord() to wrap callbacks and catch CompilerErrors

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35873).
* #35888
* #35884
* #35883
* #35882
* #35881
* #35880
* #35879
* #35878
* #35877
* #35876
* #35875
* #35874
* __->__ #35873

Changed files:
filename: compiler/fault-tolerance-overview.md
patch: @@ -31,7 +31,7 @@ Note that some errors may continue to cause an eager bailout:
 
 Add error accumulation to the `Environment` class so that any pass can record errors during compilation without halting.
 
-- [ ] **1.1 Add error accumulator to Environment** (`src/HIR/Environment.ts`)
+- [x] **1.1 Add error accumulator to Environment** (`src/HIR/Environment.ts`)
   - Add a `#errors: CompilerError` field, initialized in the constructor
   - Add a `recordError(error: CompilerDiagnostic | CompilerErrorDetail)` method that:
     - If an Invariant-category detail, immediately throw it
@@ -41,7 +41,7 @@ Add error accumulation to the `Environment` class so that any pass can record er
   - Add a `aggregateErrors(): CompilerError` method that returns the accumulated error object
   - Consider whether `recordError` should accept the same options as `CompilerError.push()` for convenience (reason, description, severity, loc, etc.)
 
-- [ ] **1.2 Add a `tryRecord` helper on Environment** (`src/HIR/Environment.ts`)
+- [x] **1.2 Add a `tryRecord` helper on Environment** (`src/HIR/Environment.ts`)
   - Add a `tryRecord(fn: () => void): void` method that wraps a callback in try/catch:
     - If `fn` throws a `CompilerError` that is NOT an invariant, record it via `recordError`
     - If `fn` throws a non-CompilerError or a CompilerError invariant, re-throw

filename: compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
patch: @@ -8,7 +8,12 @@
 import * as t from '@babel/types';
 import {ZodError, z} from 'zod/v4';
 import {fromZodError} from 'zod-validation-error/v4';
-import {CompilerError} from '../CompilerError';
+import {
+  CompilerDiagnostic,
+  CompilerError,
+  CompilerErrorDetail,
+  ErrorCategory,
+} from '../CompilerError';
 import {CompilerOutputMode, Logger, ProgramContext} from '../Entrypoint';
 import {Err, Ok, Result} from '../Utils/Result';
 import {
@@ -545,6 +550,12 @@ export class Environment {
 
   #flowTypeEnvironment: FlowTypeEnv | null;
 
+  /**
+   * Accumulated compilation errors. Passes record errors here instead of
+   * throwing, so the pipeline can continue and report all errors at once.
+   */
+  #errors: CompilerError = new CompilerError();
+
   constructor(
     scope: BabelScope,
     fnType: ReactFunctionType,
@@ -702,6 +713,75 @@ export class Environment {
     }
   }
 
+  /**
+   * Record a single diagnostic or error detail on this environment.
+   * If the error is an Invariant, it is immediately thrown since invariants
+   * represent internal bugs that cannot be recovered from.
+   * Otherwise, the error is accumulated and optionally logged.
+   */
+  recordError(error: CompilerDiagnostic | CompilerErrorDetail): void {
+    if (error.category === ErrorCategory.Invariant) {
+      const compilerError = new CompilerError();
+      if (error instanceof CompilerDiagnostic) {
+        compilerError.pushDiagnostic(error);
+      } else {
+        compilerError.pushErrorDetail(error);
+      }
+      throw compilerError;
+    }
+    if (error instanceof CompilerDiagnostic) {
+      this.#errors.pushDiagnostic(error);
+    } else {
+      this.#errors.pushErrorDetail(error);
+    }
+  }
+
+  /**
+   * Record all diagnostics from a CompilerError onto this environment.
+   */
+  recordErrors(error: CompilerError): void {
+    for (const detail of error.details) {
+      this.recordError(detail);
+    }
+  }
+
+  /**
+   * Returns true if any errors have been recorded during compilation.
+   */
+  hasErrors(): boolean {
+    return this.#errors.hasAnyErrors();
+  }
+
+  /**
+   * Returns the accumulated CompilerError containing all recorded diagnostics.
+   */
+  aggregateErrors(): CompilerError {
+    return this.#errors;
+  }
+
+  /**
+   * Wraps a callback in try/catch: if the callback throws a CompilerError
+   * that is NOT an invariant, the error is recorded and execution continues.
+   * Non-CompilerError exceptions and invariants are re-thrown.
+   */
+  tryRecord(fn: () => void): void {
+    try {
+      fn();
+    } catch (err) {
+      if (err instanceof CompilerError) {
+        // Check if any detail is an invariant — if so, re-throw
+        for (const detail of err.details) {
+          if (detail.category === ErrorCategory.Invariant) {
+            throw err;
+          }
+        }
+        this.recordErrors(err);
+      } else {
+        throw err;
+      }
+    }
+  }
+
   isContextIdentifier(node: t.Identifier): boolean {
     return this.#contextIdentifiers.has(node);
   }