Skip to content

Commit e2eaac3

Browse files
arturovtdylhunn
authored andcommitted
fix(zone.js): read Symbol.species safely (#45369)
We must read `Symbol.species` safely because `this` may be anything. For instance, `this` may be an object without a prototype (created through `Object.create(null)`); thus `this.constructor` will be undefined. One of the use cases is SystemJS creating prototype-less objects (modules) via `Object.create(null)`. The SystemJS creates an empty object and copies promise properties into that object (within the `getOrCreateLoad` function). The zone.js then checks if the resolved value has the `then` method and invokes it with the `value` context. Otherwise, this will throw an error: `TypeError: Cannot read properties of undefined (reading 'Symbol(Symbol.species)')`. PR Close #45369
1 parent 7a9c6f5 commit e2eaac3

2 files changed

Lines changed: 37 additions & 2 deletions

File tree

packages/zone.js/lib/common/promise.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,15 @@ Zone.__load_patch('ZoneAwarePromise', (global: any, Zone: ZoneType, api: _ZonePr
463463
onFulfilled?: ((value: R) => TResult1 | PromiseLike<TResult1>)|undefined|null,
464464
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>)|undefined|
465465
null): Promise<TResult1|TResult2> {
466-
let C = (this.constructor as any)[Symbol.species];
466+
// We must read `Symbol.species` safely because `this` may be anything. For instance, `this`
467+
// may be an object without a prototype (created through `Object.create(null)`); thus
468+
// `this.constructor` will be undefined. One of the use cases is SystemJS creating
469+
// prototype-less objects (modules) via `Object.create(null)`. The SystemJS creates an empty
470+
// object and copies promise properties into that object (within the `getOrCreateLoad`
471+
// function). The zone.js then checks if the resolved value has the `then` method and invokes
472+
// it with the `value` context. Otherwise, this will throw an error: `TypeError: Cannot read
473+
// properties of undefined (reading 'Symbol(Symbol.species)')`.
474+
let C = (this.constructor as any)?.[Symbol.species];
467475
if (!C || typeof C !== 'function') {
468476
C = this.constructor || ZoneAwarePromise;
469477
}
@@ -483,7 +491,8 @@ Zone.__load_patch('ZoneAwarePromise', (global: any, Zone: ZoneType, api: _ZonePr
483491
}
484492

485493
finally<U>(onFinally?: () => U | PromiseLike<U>): Promise<R> {
486-
let C = (this.constructor as any)[Symbol.species];
494+
// See comment on the call to `then` about why thee `Symbol.species` is safely accessed.
495+
let C = (this.constructor as any)?.[Symbol.species];
487496
if (!C || typeof C !== 'function') {
488497
C = ZoneAwarePromise;
489498
}

packages/zone.js/test/common/Promise.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,32 @@ describe(
116116
expect(new MyPromise(() => {}).then(() => null) instanceof MyPromise).toBe(true);
117117
});
118118

119+
it('should allow subclassing without Symbol.species if properties are copied (SystemJS case)',
120+
() => {
121+
let value: any = null;
122+
const promise = Promise.resolve();
123+
const systemjsModule = Object.create(null);
124+
125+
// We only copy properties from the `promise` instance onto the `systemjsModule` object.
126+
// This is what SystemJS is doing internally:
127+
// https://github.com/systemjs/systemjs/blob/main/src/system-core.js#L107-L113
128+
for (const property in promise) {
129+
const value: any = promise[property as keyof typeof promise];
130+
if (!(value in systemjsModule) || systemjsModule[property] !== value) {
131+
systemjsModule[property] = value;
132+
}
133+
}
134+
135+
queueZone.run(() => {
136+
Promise.resolve().then(() => systemjsModule).then((v) => (value = v));
137+
flushMicrotasks();
138+
// Note: we want to ensure that the promise has been resolved. In this specific case
139+
// the promise may resolve to different values in the browser and on the Node.js side.
140+
// SystemJS runs only in the browser and it only needs the promise to be resolved.
141+
expect(value).not.toEqual(null);
142+
});
143+
});
144+
119145
it('should allow subclassing with Symbol.species', () => {
120146
class MyPromise extends Promise<any> {
121147
constructor(fn: any) {

0 commit comments

Comments
 (0)