Visitar URL original
fix(core): untrack `onError` handler to prevent signal write exceptions · angular/angular@74bc86d · GitHub
Skip to content

Commit 74bc86d

Browse files
JeanMecheNothingEverHappens
authored andcommitted
fix(core): untrack onError handler to prevent signal write exceptions
This wraps the execution of the `onError` handlers (like `@boundary`) in `untracked` to prevent the `NG0600` exception when writing to signals. Fixes #71199. (cherry picked from commit 3be586f)
1 parent fb6f8f5 commit 74bc86d

2 files changed

Lines changed: 74 additions & 17 deletions

File tree

‎packages/core/src/render3/instructions/change_detection.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,12 @@ export function refreshView<T>(
383383
declarationType,
384384
caughtBy: onError,
385385
};
386-
onError(encapsulateBoundaryError(errorToHandle), details);
386+
const prevConsumer = setActiveConsumer(null);
387+
try {
388+
onError(encapsulateBoundaryError(errorToHandle), details);
389+
} finally {
390+
setActiveConsumer(prevConsumer);
391+
}
387392
handled = true;
388393
break;
389394
} catch (boundaryError) {

‎packages/core/test/render3/error_boundary_spec.ts‎

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {
1010
Component,
11+
ElementRef,
1112
EnvironmentInjector,
1213
ErrorHandler,
1314
Input,
@@ -16,6 +17,7 @@ import {
1617
ViewContainerRef,
1718
effect,
1819
signal,
20+
viewChild,
1921
} from '@angular/core';
2022
import {DeferBlockBehavior, DeferBlockState, TestBed} from '@angular/core/testing';
2123
import {ErrorBoundaryWrappedError, ErrorDetails} from '../../src/error_handler';
@@ -312,12 +314,12 @@ describe('Error Boundary Runtime Interception', () => {
312314
@boundary {
313315
<child-cmp-defer></child-cmp-defer>
314316
} @error (let err) {
315-
<div id="fallback">Fallback: {{err.message}}</div>
317+
<div id="fallback">Fallback: {{ err.message }}</div>
316318
}
317319
} @loading {
318320
<div id="loading">Loading...</div>
319321
}
320-
`,
322+
`,
321323
imports: [ChildCmpDefer],
322324
})
323325
class AppDeferTest {
@@ -517,7 +519,7 @@ describe('Error Boundary Runtime Interception', () => {
517519
@boundary {
518520
<ul>
519521
@for (item of items(); track trackFn(item)) {
520-
<li>{{item}}</li>
522+
<li>{{ item }}</li>
521523
}
522524
</ul>
523525
} @error {
@@ -581,7 +583,7 @@ describe('@boundary runtime instructions (JIT)', () => {
581583
<throwing-ctor></throwing-ctor>
582584
Main Content
583585
} @error (let err) {
584-
Error: {{err.message}}
586+
Error: {{ err.message }}
585587
}
586588
`,
587589
imports: [ThrowingCtor],
@@ -602,7 +604,7 @@ describe('@boundary runtime instructions (JIT)', () => {
602604
<throwing-hook [shouldThrow]="triggerError()"></throwing-hook>
603605
Main Content
604606
} @error (let err) {
605-
Error: {{err.message}}
607+
Error: {{ err.message }}
606608
}
607609
`,
608610
imports: [ThrowingHook],
@@ -629,7 +631,7 @@ describe('@boundary runtime instructions (JIT)', () => {
629631
{{ throwInBinding() }}
630632
Main Content
631633
} @error (let err) {
632-
Error: {{err.message}}
634+
Error: {{ err.message }}
633635
}
634636
`,
635637
})
@@ -662,7 +664,7 @@ describe('@boundary runtime instructions (JIT)', () => {
662664
}
663665
Main Content
664666
} @error (let err) {
665-
Error: {{err.message}}
667+
Error: {{ err.message }}
666668
}
667669
`,
668670
imports: [ThrowingCtor],
@@ -787,7 +789,7 @@ describe('@boundary runtime instructions (JIT)', () => {
787789
Main Content
788790
}
789791
} @error (let err, r = $reset) {
790-
Error: {{err.message}}
792+
Error: {{ err.message }}
791793
<button (click)="r()">Reset</button>
792794
}
793795
`,
@@ -822,7 +824,7 @@ describe('@boundary runtime instructions (JIT)', () => {
822824
@boundary {
823825
{{ throwError() }}
824826
} @error (let err) {
825-
Error: {{err.message}}
827+
Error: {{ err.message }}
826828
<button (click)="$reset()">Reset</button>
827829
}
828830
`,
@@ -866,7 +868,7 @@ describe('@boundary runtime instructions (JIT)', () => {
866868
Main Content
867869
}
868870
} @error {
869-
Error: {{$error.message}}
871+
Error: {{ $error.message }}
870872
}
871873
`,
872874
})
@@ -893,7 +895,7 @@ describe('@boundary runtime instructions (JIT)', () => {
893895
Main Content
894896
}
895897
} @error (let err, r = $reset) {
896-
Error: {{err.message}}
898+
Error: {{ err.message }}
897899
<button (click)="handleReset(r)">Reset Later</button>
898900
}
899901
`,
@@ -944,10 +946,10 @@ describe('@boundary runtime instructions (JIT)', () => {
944946
Main Content
945947
}
946948
} @error (let err; r = $reset; when isChartError(err)) {
947-
Chart Error: {{err.message}}
949+
Chart Error: {{ err.message }}
948950
<button id="reset-chart" (click)="r()">Reset</button>
949951
} @error (let error; r = $reset) {
950-
Generic Error: {{error.message}}
952+
Generic Error: {{ error.message }}
951953
<button id="reset-generic" (click)="r()">Reset</button>
952954
}
953955
`,
@@ -1002,7 +1004,7 @@ describe('@boundary runtime instructions (JIT)', () => {
10021004
{{ throwError() }}
10031005
}
10041006
} @error (let err; when isChartError(err)) {
1005-
Chart Error: {{err.message}}
1007+
Chart Error: {{ err.message }}
10061008
}
10071009
`,
10081010
})
@@ -1039,10 +1041,10 @@ describe('@boundary runtime instructions (JIT)', () => {
10391041
@Component({
10401042
template: `
10411043
@boundary {
1042-
<throwing-effect/>
1044+
<throwing-effect />
10431045
Main Content
10441046
} @error (let err) {
1045-
Error: {{err.message}}
1047+
Error: {{ err.message }}
10461048
}
10471049
`,
10481050
imports: [ThrowingEffect],
@@ -1103,4 +1105,54 @@ describe('@boundary runtime instructions (JIT)', () => {
11031105
expect(capturedDetails.boundary!.type).toBe(Host);
11041106
expect(typeof capturedDetails.boundary!.reset).toBe('function');
11051107
});
1108+
1109+
it('should allow signal writes in ErrorHandler and view queries when a view inside @boundary throws', async () => {
1110+
const errors = signal<unknown[]>([]);
1111+
1112+
class SignalWritingErrorHandler implements ErrorHandler {
1113+
handleError(error: unknown) {
1114+
errors.update((list) => [...list, error]);
1115+
}
1116+
}
1117+
1118+
@Component({
1119+
selector: 'app-throws',
1120+
template: '{{ boom() }}',
1121+
})
1122+
class Throws {
1123+
boom(): string {
1124+
throw new Error('original');
1125+
}
1126+
}
1127+
1128+
@Component({
1129+
template: `
1130+
@boundary {
1131+
@if (true) {
1132+
<app-throws />
1133+
}
1134+
} @error {
1135+
<p>{{ $error.message }}</p>
1136+
}
1137+
<div #anchor></div>
1138+
`,
1139+
imports: [Throws],
1140+
})
1141+
class App {
1142+
anchor = viewChild<ElementRef>('anchor');
1143+
}
1144+
1145+
TestBed.configureTestingModule({
1146+
providers: [{provide: ErrorHandler, useClass: SignalWritingErrorHandler}],
1147+
});
1148+
1149+
const fixture = TestBed.createComponent(App);
1150+
await fixture.whenStable();
1151+
1152+
// The error should have been caught and the @error block should be rendered.
1153+
expect(fixture.nativeElement.textContent).toContain('original');
1154+
// The signal writing ErrorHandler should have successfully written to the signal
1155+
expect(errors().length).toBe(1);
1156+
expect((errors()[0] as Error).message).toBe('original');
1157+
});
11061158
});

0 commit comments

Comments
 (0)