Skip to content

fix(zone.js): allow draining microtasks in Promise.then (through flag)#68134

Merged
kirjs merged 1 commit into
angular:mainfrom
arturovt:fix/zone_44446_drain_v2
Apr 15, 2026
Merged

fix(zone.js): allow draining microtasks in Promise.then (through flag)#68134
kirjs merged 1 commit into
angular:mainfrom
arturovt:fix/zone_44446_drain_v2

Conversation

@arturovt

@arturovt arturovt commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

These changes are essentially the same as those introduced in #45273, but they include backward compatibility for applications that explicitly rely on the order in which microtasks are drained.

This is critically important for our code and other third-party code, which is beyond our control, to work properly. If a microtask is scheduled within an event listener to be executed "later", it should indeed be executed later and not synchronously, as this would break the expected flow of code execution.

The simple code that reproduces the behavior that exists now:

Zone.current.fork({name: 'child'}).run(() => {
  const div = document.createElement('div');
  div.style.height = '200px';
  div.style.width = '200px';
  div.style.backgroundColor = 'red';
  document.body.appendChild(div);

  function listener() {
    Promise.resolve().then(() => {
      div.style.height = '400px';
    });
  }

  div.addEventListener('fakeEvent', listener);
  div.dispatchEvent(new Event('fakeEvent'));
  console.log(div.getBoundingClientRect().height); // 400
});

The code above logs 400 as the height, but it should actually log 200 because the height is updated in a microtask within the event listener.

When using Angular with microfrontend applications, especially when other apps might be using React, zone.js can disrupt the classical order of operations. For example, when using a react-component/trigger, it schedules a microtask within an event listener using Promise.resolve().then(...) to determine whether the event needs to be re-dispatched. The event is re-dispatched when the layout has changed, which is why a microtask is used.

With this change, we introduce a global configuration flag, __zone_symbol__enable_native_microtask_draining, to allow consumers to enable microtask draining within a browser microtask.

This flag is necessary to prevent any breaking changes resulting from this modification. The previous attempt to address this issue caused a significant number of failures in g3. Therefore, we are hiding that fix behind the configuration flag.

Closes #44446
Closes #55590
Closes #51328

@pullapprove
pullapprove Bot requested a review from crisbeto April 10, 2026 21:26
@angular-robot angular-robot Bot added the area: zones Issues related to zone.js label Apr 10, 2026
@ngbot ngbot Bot added this to the Backlog milestone Apr 10, 2026
@JeanMeche
JeanMeche requested a review from atscott April 10, 2026 22:29
Comment on lines +1503 to +1507
// To prevent any breaking changes resulting from this change, given that
// it was already causing a significant number of failures in g3, we have hidden
// that behavior behind a global configuration flag. Consumers can enable this
// flag explicitly if they want the microtask queue to be drained as defined
// in the specification.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it wasn't a significant number (IIRC, like 10) but more that there were a few and it's really hard to tell if something just isn't covered by tests and is going to cause a production outage.

Comment on lines +1567 to +1570
// The order matters!
if (global[enableNativeMicrotaskDraining]) {
_isDrainingMicrotaskQueue = false;
_api.microtaskDrainDone();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't specifically recall why order matters and it does look odd that it's different from the other case. do you remember why and can you add more details?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't recall too, it didn't have any issues tbh. I think it should always be the following:

_isDrainingMicrotaskQueue = false;
_api.microtaskDrainDone();

Regardless whether microtaskDrainDone would throw.

@arturovt

Copy link
Copy Markdown
Contributor Author

@atscott should __zone_symbol__enable_native_microtask_draining be documented as the recommended configuration for existing apps using zone.js?

The flag eliminates a whole class of subtle bugs — flaky ordering in router navigation, unexpected Promise.then sequencing in event handlers, etc.

@arturovt
arturovt force-pushed the fix/zone_44446_drain_v2 branch from 948ccc7 to acf8233 Compare April 11, 2026 16:23
@arturovt

Copy link
Copy Markdown
Contributor Author

@atscott I also added more regression tests for other issues being closed with this fix.

These changes are essentially the same as those introduced in
angular#45273, but they include backward compatibility
for applications that explicitly rely on the order in which microtasks are drained.

This is critically important for our code and other third-party code, which is
beyond our control, to work properly. If a microtask is scheduled within an event
listener to be executed "later", it should indeed be executed later and not synchronously,
as this would break the expected flow of code execution.

The simple code that reproduces the behavior that exists now:

```ts
Zone.current.fork({name: 'child'}).run(() => {
  const div = document.createElement('div');
  div.style.height = '200px';
  div.style.width = '200px';
  div.style.backgroundColor = 'red';
  document.body.appendChild(div);

  function listener() {
    Promise.resolve().then(() => {
      div.style.height = '400px';
    });
  }

  div.addEventListener('fakeEvent', listener);
  div.dispatchEvent(new Event('fakeEvent'));
  console.log(div.getBoundingClientRect().height); // 400
});
```

The code above logs 400 as the height, but it should actually log 200 because the
height is updated in a microtask within the event listener.

When using Angular with microfrontend applications, especially when other apps might be
using React, zone.js can disrupt the classical order of operations. For example, when using a
`react-component/trigger`, it schedules a microtask within an event listener using
`Promise.resolve().then(...)` to determine whether the event needs to be re-dispatched.
The event is re-dispatched when the layout has changed, which is why a microtask is used.

With this change, we introduce a global configuration flag,
`__zone_symbol__enable_native_microtask_draining`, to allow consumers to enable
microtask draining within a browser microtask.

This flag is necessary to prevent any breaking changes resulting from this modification.
The previous attempt to address this issue caused a significant number of failures in g3.
Therefore, we are hiding that fix behind the configuration flag.

Closes angular#44446
Closes angular#55590
Closes angular#51328
@arturovt
arturovt force-pushed the fix/zone_44446_drain_v2 branch from acf8233 to 45a4650 Compare April 11, 2026 16:26
@thePunderWoman

Copy link
Copy Markdown
Contributor

Woah, looks like you've opened a lot of issues/PRs recently. While we appreciate contributions from the community, triaging and reviewing a large influx of content in a short time period takes time away from other ongoing projects. As a result, we're closing these issues/PRs to maintain the team's focus.

Note that this is not necessarily a rejection of the goals or direction of any of these contributions in particular, so much as a reflection of the team's current capacity and priorities.

You are welcome to open a smaller subset of issues/PRs in accordance with our policy focused on the most important and impactful contributions and we will do our best to prioritize a response as soon as possible.

@atscott atscott reopened this Apr 13, 2026
@atscott

atscott commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

should __zone_symbol__enable_native_microtask_draining be documented as the recommended configuration for existing apps using zone.js?

I wouldn't make a hard push for it. While I agree that it should be the default, it's still likely not worth doing anything about for existing apps, especially given that very few apps are actually at risk of hitting this issue.

@atscott atscott added the target: patch This PR is targeted for the next patch release label Apr 13, 2026
@atscott

atscott commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

@atscott atscott added the action: merge The PR is ready for merge by the caretaker label Apr 14, 2026
@JeanMeche
JeanMeche removed the request for review from crisbeto April 15, 2026 08:11
@kirjs
kirjs merged commit fc6a7ee into angular:main Apr 15, 2026
26 checks passed
@kirjs

kirjs commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

This PR was merged into the repository. The changes were merged into the following branches:

@arturovt
arturovt deleted the fix/zone_44446_drain_v2 branch April 15, 2026 18:16
@angular-automatic-lock-bot

Copy link
Copy Markdown

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot Bot locked and limited conversation to collaborators May 16, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

action: merge The PR is ready for merge by the caretaker area: zones Issues related to zone.js target: patch This PR is targeted for the next patch release

Projects

None yet

4 participants