[compiler] Phase 1: Add error accumulation infrastructure to Environment (#35873)
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
Joseph Savona committed
Feb 23, 2026 at 15:18 UTC
eca778cf8bf88b2a62a5244f94a0084593ffbf14
2 files changed
+83
-3
compiler/fault-tolerance-overview.md
+2
-2
@@ -31,7 +31,7 @@ Note that some errors may continue to cause an eager bailout:
31
32
Add error accumulation to the `Environment` class so that any pass can record errors during compilation without halting.
33
34
-- [ ] **1.1 Add error accumulator to Environment** (`src/HIR/Environment.ts`)
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
@@ -41,7 +41,7 @@ Add error accumulation to the `Environment` class so that any pass can record er
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
-- [ ] **1.2 Add a `tryRecord` helper on Environment** (`src/HIR/Environment.ts`)
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
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+81
-1
@@ -8,7 +8,12 @@
8
import * as t from '@babel/types';
9
import {ZodError, z} from 'zod/v4';
10
import {fromZodError} from 'zod-validation-error/v4';
11
-import {CompilerError} from '../CompilerError';
11
+import {
12
+ CompilerDiagnostic,
13
+ CompilerError,
14
+ CompilerErrorDetail,
15
+ ErrorCategory,
16
+} from '../CompilerError';
17
import {CompilerOutputMode, Logger, ProgramContext} from '../Entrypoint';
18
import {Err, Ok, Result} from '../Utils/Result';
19
import {
@@ -545,6 +550,12 @@ export class Environment {
550
551
#flowTypeEnvironment: FlowTypeEnv | null;
552
553
+ /**
554
+ * Accumulated compilation errors. Passes record errors here instead of
555
+ * throwing, so the pipeline can continue and report all errors at once.
556
+ */
557
+ #errors: CompilerError = new CompilerError();
558
+
559
constructor(
560
scope: BabelScope,
561
fnType: ReactFunctionType,
@@ -702,6 +713,75 @@ export class Environment {
713
}
714
}
715
716
+ /**
717
+ * Record a single diagnostic or error detail on this environment.
718
+ * If the error is an Invariant, it is immediately thrown since invariants
719
+ * represent internal bugs that cannot be recovered from.
720
+ * Otherwise, the error is accumulated and optionally logged.
721
+ */
722
+ recordError(error: CompilerDiagnostic | CompilerErrorDetail): void {
723
+ if (error.category === ErrorCategory.Invariant) {
724
+ const compilerError = new CompilerError();
725
+ if (error instanceof CompilerDiagnostic) {
726
+ compilerError.pushDiagnostic(error);
727
+ } else {
728
+ compilerError.pushErrorDetail(error);
729
+ }
730
+ throw compilerError;
731
+ }
732
+ if (error instanceof CompilerDiagnostic) {
733
+ this.#errors.pushDiagnostic(error);
734
+ } else {
735
+ this.#errors.pushErrorDetail(error);
736
+ }
737
+ }
738
+
739
+ /**
740
+ * Record all diagnostics from a CompilerError onto this environment.
741
+ */
742
+ recordErrors(error: CompilerError): void {
743
+ for (const detail of error.details) {
744
+ this.recordError(detail);
745
+ }
746
+ }
747
+
748
+ /**
749
+ * Returns true if any errors have been recorded during compilation.
750
+ */
751
+ hasErrors(): boolean {
752
+ return this.#errors.hasAnyErrors();
753
+ }
754
+
755
+ /**
756
+ * Returns the accumulated CompilerError containing all recorded diagnostics.
757
+ */
758
+ aggregateErrors(): CompilerError {
759
+ return this.#errors;
760
+ }
761
+
762
+ /**
763
+ * Wraps a callback in try/catch: if the callback throws a CompilerError
764
+ * that is NOT an invariant, the error is recorded and execution continues.
765
+ * Non-CompilerError exceptions and invariants are re-thrown.
766
+ */
767
+ tryRecord(fn: () => void): void {
768
+ try {
769
+ fn();
770
+ } catch (err) {
771
+ if (err instanceof CompilerError) {
772
+ // Check if any detail is an invariant — if so, re-throw
773
+ for (const detail of err.details) {
774
+ if (detail.category === ErrorCategory.Invariant) {
775
+ throw err;
776
+ }
777
+ }
778
+ this.recordErrors(err);
779
+ } else {
780
+ throw err;
781
+ }
782
+ }
783
+ }
784
+
785
isContextIdentifier(node: t.Identifier): boolean {
786
return this.#contextIdentifiers.has(node);
787
}