Skip to content

ng15 compiler output broken this in async field functions, with "useDefineForClassFields" unset #24507

Closed as not planned
@johncrim

Description

@johncrim

Which @angular/* package(s) are the source of the bug?

compiler

Is this a regression?

Yes

Description

After upgrading to ng15, I'm seeing bad compile output in async functions that are assigned to fields - the this is not being captured, resulting in NPEs in a number of places. I've also seen other issues where field initialization is re-ordered after upgrading to ng15, which may be related.

A simple example is:

import { Inject, Injectable, InjectionToken, OnDestroy } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { delay } from './delay';

export const WINDOW = new InjectionToken<Window>('window', { providedIn: 'platform', factory: () => window });

/**
 * Service that provides online/offline status.
 */
@Injectable({ providedIn: 'root' })
export class OnlineService2
  extends BehaviorSubject<boolean>
  implements OnDestroy {

  constructor(@Inject(WINDOW) private readonly _window: Window) {
    const browserOnline = _window.navigator.onLine;
    super(browserOnline);

    _window.addEventListener('online', this.toOnline);
    _window.addEventListener('offline', this.toOffline);
  }

  ngOnDestroy() {
    this._window.removeEventListener('online', this.toOnline);
    this._window.removeEventListener('offline', this.toOffline);
    this.complete();
  }

  private readonly toOnline = async () => {
    await delay(1);
    this.next(true);
  };

  private readonly toOffline = async () => {
    await delay(1);
    this.next(false);
  };
}

The compiled output is:

class OnlineService2 extends rxjs__WEBPACK_IMPORTED_MODULE_1__.BehaviorSubject {
  constructor() {
    const browserOnline = navigator.onLine;
    super(browserOnline);
    window.addEventListener('online', this.toOnline);
    window.addEventListener('offline', this.toOffline);
  }
  ngOnDestroy() {
    window.removeEventListener('online', this.toOnline);
    window.removeEventListener('offline', this.toOffline);
    this.complete();
  }
  toOnline = (0,prj_node_modules_angular_devkit_build_angular_node_modules_babel_runtime_helpers_esm_asyncToGenerator_js__WEBPACK_IMPORTED_MODULE_0__["default"])(function* () {
    yield (0,_ease_util__WEBPACK_IMPORTED_MODULE_2__.delay)(1);
    _this.next(true);
  });
  toOffline = (0,prj_node_modules_angular_devkit_build_angular_node_modules_babel_runtime_helpers_esm_asyncToGenerator_js__WEBPACK_IMPORTED_MODULE_0__["default"])(function* () {
    yield (0,_ease_util__WEBPACK_IMPORTED_MODULE_2__.delay)(1);
    _this2.next(false);
  });
  static ɵfac = function OnlineService2_Factory(t) {
    return new (t || OnlineService2)();
  };
  static ɵprov = /*@__PURE__*/_angular_core__WEBPACK_IMPORTED_MODULE_3__["ɵɵdefineInjectable"]({
    token: OnlineService2,
    factory: OnlineService2.ɵfac,
    providedIn: 'root'
  });
}

You can see that the _this and _this2 are not initialized before they are used. If I change my tsconfig settings from:

    "module": "esnext",

to

    "module": "ES2022",
    "useDefineForClassFields": false,

the compiled output works as expected:

class OnlineService2 extends rxjs__WEBPACK_IMPORTED_MODULE_2__.BehaviorSubject {
  constructor(_window) {
    var _this;
    const browserOnline = _window.navigator.onLine;
    super(browserOnline);
    _this = this;
    this._window = _window;
    this.toOnline = /*#__PURE__*/(0,prj_node_modules_angular_devkit_build_angular_node_modules_babel_runtime_helpers_esm_asyncToGenerator_js__WEBPACK_IMPORTED_MODULE_0__["default"])(function* () {
      yield (0,_ease_util__WEBPACK_IMPORTED_MODULE_3__.delay)(1);
      _this.next(true);
    });
    this.toOffline = /*#__PURE__*/(0,prj_node_modules_angular_devkit_build_angular_node_modules_babel_runtime_helpers_esm_asyncToGenerator_js__WEBPACK_IMPORTED_MODULE_0__["default"])(function* () {
      yield (0,_ease_util__WEBPACK_IMPORTED_MODULE_3__.delay)(1);
      _this.next(false);
    });
    _window.addEventListener('online', this.toOnline);
    _window.addEventListener('offline', this.toOffline);
  }
  ngOnDestroy() {
    this._window.removeEventListener('online', this.toOnline);
    this._window.removeEventListener('offline', this.toOffline);
    this.complete();
  }
  static #_ = this.ɵfac = function OnlineService2_Factory(t) {
    return new (t || OnlineService2)(_angular_core__WEBPACK_IMPORTED_MODULE_1__["ɵɵinject"](WINDOW));
  };
  static #_2 = this.ɵprov = /*@__PURE__*/_angular_core__WEBPACK_IMPORTED_MODULE_1__["ɵɵdefineInjectable"]({
    token: OnlineService2,
    factory: OnlineService2.ɵfac,
    providedIn: 'root'
  });
}

Please provide a link to a minimal reproduction of the bug

https://github.com/johncrim/repro-ng15-compile

Please provide the exception or error you saw

Error: Uncaught (in promise): ReferenceError: _this is not defined
ReferenceError: _this is not defined
    at http://localhost:9876/_karma_webpack_/webpack:/apps/ease/src/modules/app/Online.service2.ts:37:5
    at Generator.next (<anonymous>)
    at asyncGeneratorStep (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@angular-devkit/build-angular/node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js:3:1)
    at apply (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@angular-devkit/build-angular/node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js:22:1)
    at _ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:375:26)
    at FakeAsyncTestZoneSpec.onInvoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone-testing.js:1938:33)
    at ProxyZoneSpec.onInvoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone-testing.js:284:39)
    at _ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:374:52)
    at Zone.run (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:134:43)
    at apply (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:1278:36)
    at _ZoneDelegate.invokeTask (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:409:31)
    at ProxyZoneSpec.onInvokeTask (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone-testing.js:318:39)
    at _ZoneDelegate.invokeTask (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:408:60)
    at Zone.runTask (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:178:47)
    at invokeTask (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:490:34)

Please provide the environment you discovered this bug in (run ng version)

Angular CLI: 15.0.5
Node: 19.3.0 (Unsupported)
Package Manager: yarn 3.2.4
OS: win32 x64

Angular: 15.0.4
... animations, cdk, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1500.5
@angular-devkit/build-angular   15.0.5
@angular-devkit/core            15.0.5
@angular-devkit/schematics      15.0.5
@angular/cli                    15.0.5
@schematics/angular             15.0.5
ng-packagr                      15.0.3
rxjs                            7.8.0
typescript                      4.8.4
webpack                         5.75.0

Anything else?

I have not been able to cause the error in my standalone project (https://github.com/johncrim/repro-ng15-compile) - I've tried tweaking dependencies eg the typescript version and tsconfig setting to match my upgraded project, but no luck generating the failure. However if I move the same code into my project I am seeing the compile output described above.

I have a fix (in tsconfig set "module": "ES2022" and "useDefineForClassFields": false) which is easy to apply, though I'm not done testing it everywhere. Since those tsconfig settings weren't changed by migrations I'm raising the issue for the benefit of the community.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions