fix(ivy): don't mask errors by calling lifecycle hooks after a crash (#31244)
The Angular runtime frequently calls into user code (for example, when writing to a property binding). Since user code can throw errors, calls to it are frequently wrapped in a try-finally block. In Ivy, the following pattern is common: ```typescript enterView(); try { callUserCode(); } finally { leaveView(); } ``` This has a significant problem, however: `leaveView` has a side effect: it calls any pending lifecycle hooks that might've been scheduled during the current round of change detection. Generally it's a bad idea to run lifecycle hooks after the application has crashed. The application is in an inconsistent state - directives may not be instantiated fully, queries may not be resolved, bindings may not have been applied, etc. Invariants that the app code relies upon may not hold. Further crashes or broken behavior are likely. Frequently, lifecycle hooks are used to make assertions about these invariants. When these assertions fail, they will throw and "swallow" the original error, making debugging of the problem much more difficult. This commit modifies `leaveView` to understand whether the application is currently crashing, via a parameter `safeToRunHooks`. This parameter is set by modifying the above pattern: ```typescript enterView(); let safeToRunHooks = false; try { callUserCode(); safeToRunHooks = true; } finally { leaveView(..., safeToRunHooks); } ``` If `callUserCode` crashes, then `safeToRunHooks` will never be set to `true` and `leaveView` won't call any further user code. The original error will then propagate back up the stack and be reported correctly. A test is added to verify this behavior. PR Close #31244
This commit is contained in:

committed by
Kara Erickson

parent
f690a4e0af
commit
32c760f5e7
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {Component, ContentChild, Directive, ElementRef, EventEmitter, HostBinding, HostListener, Input, OnInit, Output, QueryList, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core';
|
||||
import {Component, ContentChild, Directive, ElementRef, EventEmitter, HostBinding, HostListener, Input, NgModule, OnInit, Output, QueryList, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core';
|
||||
import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {By} from '@angular/platform-browser';
|
||||
@ -1737,4 +1737,34 @@ describe('acceptance integration tests', () => {
|
||||
expect(clicks).toBe(1);
|
||||
});
|
||||
|
||||
it('should not mask errors thrown during lifecycle hooks', () => {
|
||||
@Directive({
|
||||
selector: '[dir]',
|
||||
inputs: ['dir'],
|
||||
})
|
||||
class Dir {
|
||||
get dir(): any { return null; }
|
||||
|
||||
set dir(value: any) { throw new Error('this error is expected'); }
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<div [dir]="3"></div>',
|
||||
})
|
||||
class Cmp {
|
||||
ngAfterViewInit(): void {
|
||||
// This lifecycle hook should never run, since attempting to bind to Dir's input will throw
|
||||
// an error. If the runtime continues to run lifecycle hooks after that error, then it will
|
||||
// execute this hook and throw this error, which will mask the real problem. This test
|
||||
// verifies this don't happen.
|
||||
throw new Error('this error is unexpected');
|
||||
}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [Cmp, Dir],
|
||||
});
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
expect(() => fixture.detectChanges()).toThrowError('this error is expected');
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user