angular: angular with tsconfig target ES2017 async/await will not work with zone.js

this issue is similar with #715, if we use chrome v8 async/await and compile angular with tsconfig target ‘ES2017’, then typescript will not generate __awaiter code and use native async/await. and the following logic will fail

(click) = test();
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
const test = async () => {
      console.log(Zone.current.name) // will output 'angular'
      await delay(100);
      console.log(Zone.current.name) // will output 'root'
    }

unlike typescript transpiler, native async/await will first yield from test, and then call promise.then for continuation when await something. So Zone currentFrame will become root. The sequence of above logic when call await delay(100) will look like

  1. delay function return a ZoneAwarePromise, Zone.current is angular
  2. test function return native Promise which generate from chrome v8 by await, and test() execution is finished.
  3. So Zone.currentZone is transite from angular->root
  4. ZoneAwarePromise which generated from step1 was chained by called by Promise.prototype.then
  5. delay timeout is executed, and ZoneAwarePromise resolved
  6. the chained Promise is resolved , but the Zone.current is root.

Based on the spec, https://tc39.github.io/ecmascript-asyncawait/#abstract-ops-async-function-await


1. Let asyncContext be the running execution context.
2. Let promiseCapability be ! NewPromiseCapability(%Promise%).
3. Let resolveResult be ! Call(promiseCapability.[[Resolve]], undefined, « value »).
4. Let onFulfilled be a new built-in function object as defined in AsyncFunction Awaited Fulfilled.
5. Let onRejected be a new built-in function object as defined in AsyncFunction Awaited Rejected.
6. Set onFulfilled and onRejected's [[AsyncContext]] internal slots to asyncContext.
7. Let throwawayCapability be NewPromiseCapability(%Promise%).
8. Perform ! PerformPromiseThen(promiseCapability.[[Promise]], onFulfilled, onRejected, throwawayCapability).
9. Remove asyncContext from the execution context stack and restore the execution context that is at the top of the execution context stack as the running execution context.
10. Set the code evaluation state of asyncContext such that when evaluation is resumed with a Completion resumptionValue the following steps will be performed:
11. Return resumptionValue.
12. Return.

Step8 is not be executed immediately but in the microTask queue after the current function execution.

I checked Chrome v8 source, https://chromium.googlesource.com/v8/v8/+/refs/heads/5.5.10/src/js/promise.js

function ResolvePromise(promise, resolution) {
  if (resolution === promise) {
    return RejectPromise(promise,
                         %make_type_error(kPromiseCyclic, resolution),
                         true);
  }
  if (IS_RECEIVER(resolution)) {
    // 25.4.1.3.2 steps 8-12
    try {
      var then = resolution.then;
    } catch (e) {
      return RejectPromise(promise, e, true);
    }
    // Resolution is a native promise and if it's already resolved or
    // rejected, shortcircuit the resolution procedure by directly
    // reusing the value from the promise.
    if (IsPromise(resolution) && then === PromiseThen) {
      var thenableState = GET_PRIVATE(resolution, promiseStateSymbol);
      if (thenableState === kFulfilled) {
        // This goes inside the if-else to save one symbol lookup in
        // the slow path.
        var thenableValue = GET_PRIVATE(resolution, promiseResultSymbol);
        FulfillPromise(promise, kFulfilled, thenableValue,
                       promiseFulfillReactionsSymbol);
        SET_PRIVATE(promise, promiseHasHandlerSymbol, true);
        return;
      } else if (thenableState === kRejected) {
        var thenableValue = GET_PRIVATE(resolution, promiseResultSymbol);
        if (!HAS_DEFINED_PRIVATE(resolution, promiseHasHandlerSymbol)) {
          // Promise has already been rejected, but had no handler.
          // Revoke previously triggered reject event.
          %PromiseRevokeReject(resolution);
        }
        // Don't cause a debug event as this case is forwarding a rejection
        RejectPromise(promise, thenableValue, false);
        SET_PRIVATE(resolution, promiseHasHandlerSymbol, true);
        return;
      }
    }
    if (IS_CALLABLE(then)) {
      // PromiseResolveThenableJob
      var id;
      var name = "PromiseResolveThenableJob";
      var instrumenting = DEBUG_IS_ACTIVE;
      %EnqueueMicrotask(function() {
        if (instrumenting) {
          %DebugAsyncTaskEvent({ type: "willHandle", id: id, name: name });
        }
        // These resolving functions simply forward the exception, so
        // don't create a new debugEvent.
        var callbacks = CreateResolvingFunctions(promise, false);
        try {
          %_Call(then, resolution, callbacks.resolve, callbacks.reject);
        } catch (e) {
          %_Call(callbacks.reject, UNDEFINED, e);
        }
        if (instrumenting) {
          %DebugAsyncTaskEvent({ type: "didHandle", id: id, name: name });
        }
      });
      if (instrumenting) {
        id = ++lastMicrotaskId;
        %DebugAsyncTaskEvent({ type: "enqueue", id: id, name: name });
      }
      return;
    }
  }
  FulfillPromise(promise, kFulfilled, resolution, promiseFulfillReactionsSymbol);
}

ZoneAwarePromise is not treated as native one, so Chrome v8 enqueue a micro task to perform then call. This maybe the reason.

And it seems the logic is totally changed in v8 6.0, so I will try the chromium 6.0 to see what happened.

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 86
  • Comments: 154 (50 by maintainers)

Commits related to this issue

Most upvoted comments

In the Chrome Dev Summit yesterday, the Transitioning to Modern Javascript talk encouraged developers to target ES2017, as a sweet spot with small size and 95+% browser support. So this issue may get more attention than it did previously, thanks to improved awareness.

I get it that with current state of browsers this is not possible to fix by Angular team. However, it’s a huge issue going forward with Angular lacking support for not-really-that-recent ES standards. As I get it, Google uses Angular a lot internally, there is some MS involvement as well and I get also that this issue requires “political” actions, not technical ones.

I hope people on Angular team understand how severe problems this little tiny issue #740 is going to cause moving forward, if those “political issues” to push browser support are not taken seriously by some more powerful entities within Google and maybe within MS as well. If those two giants were serious about the issue, it would be already fixed by now.

For now, the obvious workaround for the issue is just sitting on ES2015 target forever with horrendous generated async/await code, lack of optimizations and lack of proper debugging due to that generated code.

If we are just sitting and waiting “Zones for JavaScript” proposal to get anywhere from stage 0 of the TC39 process, after that was presented at the January 2016 TC39 meeting, we can be just waiting forever, without any hope about Angular ever supporting web standards going forward. You don’t possess the power, but your bosses, or bosses of your bosses, or bosses of bosses of your bosses do possess the power, so please, push them hard!

@mhevery

Has there been any progress on this one? As far as my limited skill of perception can see, this is the main issue that prevents angulars change detection work properly with es2017’s async/await.

ES2017 is realy helpfull for debugging as the compiled sourcecode by typescript/angular is not that different to the real source and so sourcemaps work much much better.

Thanks for your time, Tim.

zonejs was a mistake.

I’m going to approach your post as an Angular (frontend) developer. As I’ve noted above, Zone.js is far more useful outside of Angular than in, but it’s clear you are coming at it from that angle, and there’s plenty to discuss.

Before I get into a detailed response, I’ll note that I’ve heard all this before, many times. What I haven’t heard is folks with a deep understanding of Zone.js and the real down-to-the-metal performance impacts of Angular as a framework weigh in too often. I hope my two cents is useful as someone who has built heavily performance intensive apps; I have built several realtime audio/video processing apps for the broadcast industry built with Angular. I’m not saying that any of what you’ve said is necessarily incorrect, just that it feels misguided to me, and doesn’t fit my experience of the technology stack based on the projects I’ve worked on.

It’s complex

This word is doing a lot here. Let me address it this way: As an Angular app developer, when it comes to Zone.js and how its used in Angular, I only care about the following things:

  1. I care when Zone.js is not performant When this happens, one should escape from Zone.js using NgZone#runOutsideAngular(). That is what it’s for. This is relevant is when you are doing highly performance sensitive work such as processing audio frames, updating meters, rendering canvases, or other requestAnimationFrame style use cases. Escape from the zone, and return using NgZone#run(). Don’t ignore the tools the Angular team has given you. What I’ve found is that in these scenarios the real problem is Angular’s change detection itself. When you are solving those problems, you sometimes have to omit using Angular’s model binding system entirely to accomplish the goal. In that context, using Zone.js to handle change detection for a settings dialog is not the bottleneck.

  2. I care when Zone.js impacts debuggability Zone.js definitely impacts debuggability. Stack traces are difficult. But not unworkably so. Throwing the baby out with the bathwater for this point seems woefully short sighted.

  3. I care when my code escapes from its Zone When Zone is not adequately handling the available async-capable APIs, this happens. More on this below.

adds weight

For change detection, you are correct. But this isn’t going to be relevant when you are doing the typical Angular app- it will be relevant when you are doing requestAnimationFrame(), tight timers, or other code which frequently waits for promises to complete. When this happens, do the above: Escape from the zone, and return to do change detection. Don’t rearchitect your entire application simply to serve your 1% use case- most of your app is forms over data, and Zone.js’ change detection strategy works for that. The escape is there for the 1% where it doesn’t.

requires to be maintained.

Maintained as new async web APIs are added yes. Not maintained by the app developer of course, but maintained by the Zone.js developers themselves. So then the question becomes, how often are new async opportunities added to Web APIs? Well, you can look at the commits in https://github.com/angular/angular/commits/master/packages/zone.js where Jia Li has been maintaining Zone.js, and really there hasn’t been much needed to keep it working (with the async/await-pocalypse the main exception). Seems like more work has gone into Bazel and testing which was going to be done regardless of the merits of Zone.js as a library.

It’s a monkey patching. So by definition not a forward-looking idea.

Monkey patching is adding new user-land functionality to existing interfaces. Zone.js doesn’t do this, it instruments those interfaces. I would agree if Zone added new methods to String, or modified the parameters that Regexp took. But this isn’t the case.

It might potentially make the patched functions work differently in comparison with native/original behavior

This is possible, and I think we’d all like to see Zones as a language primitive where it has no chance of this happening, but the runtime timing model of Javascript is such that there ultimately are only 3 critical concepts: The macrotask, the microtask, and the turn. This is how every Javascript runtime you’ve ever used is constructed, and Zone.js only needs to layer on instrumentation for recording when these concepts are scheduled and when they complete. Thus, Zone.js need only express the scheduling and macro/micro task behavior of the API it is patching. It’s complex when you look under the hood, but the runtime semantics of setTimeout don’t change with every newly published ES release, so the chance of breaking semantics is rather low for a given API once the patching is stable. Would love to hear about concrete cases where you’ve seen Zone.js modify runtime semantics (other than stack traces 😅) in a way that impacted your business logic.

and also might affect the performance.

Yes, but as I’ve noted in previous posts and have hinted at above, it is easy to equate the cost of Angular’s change detection cycle with Zone.js itself. Remember, all Zone.js does is let Angular know when your callback is done executing, it is the actual change detection process which is expensive. If you don’t believe me that Zone.js isn’t dramatically impacting your frontend performance, just try this: Take your app that uses reactive stores with onPush and add Zone.js back in in your main.ts, just as an experiment:

import 'zone.js';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

let zone = Zone.current.fork();

zone.run(() => {
    platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err => console.error(err));
})

Technically speaking, running Angular in a Zone isn’t even required for Zone to be active. Just loading it will cause all the patches to be in place. Nonetheless, I’m quite certain that almost all of the perceived performance issues you are referring to will be gone, because the expensive part is change detection, not Zone.js itself. And this is why disabling change detection with NgZone#runOutsideAngular is so useful for managing performance overhead, not because it eliminates the overhead of Zone.js, but because it eliminates the overhead of extra change detection cycles.

Async/await is/was the first blocker, there might be more in the future.

I’ll grant you that it is possible a future feature could cause a problem, but that’s not a reason to ignore the use cases that only Zone.js or a similar context tracing / observability solution enables.

There is no such thing as an interface put on top of implementation. So if you started using it from day one there won’t be a simple way to replace the change detection by changing the interface implementation without a need to rethink and rewrite a huge part of the code base. No interface and so no simple/static/type-safe way to detect all the places where the change detection is being triggered. So you got Angular built on DI and other enterprise-grade fancy ideas but here you left with just that “implicit magic”.

This is only a major concern if you are thinking that you will need to remove Zone.js for either (A) performance reasons (see above) or (B) compatibility problems causing zone escape, of which (other than bugs found and fixed in Zone.js along the way) there has really only been one existential problem: async/await. In the case of bugs with Zone.js that have existed before which may have allowed your callbacks to modify Angular’s model state without a corresponding change detection cycle, you always had the option of re-entering the zone in that specific circumstance to rectify the issue. So far it has only been async/await which has not had an easy straightforward fix.

It’s enabled by default and so most of the devs likely just go with it without a doubt.

It’s the default because most apps are forms over data. The “just work” part of it has always been Angular’s promise, and Zone.js’ use in Angular 2+ was just the next obvious step. If you remember back in the Angular.js days, you might remember that the built-in HTTP service would trigger $digest automatically after your callback handled the HTTP response. Zone.js was just the next evolution of this, providing an “it just works” experience in the apps which Angular was originally designed to create.

It hides how things work from those who just go with defaults.

This is what a framework is for. Angular also hides how efficient virtual DOM diffing works, and how the dependency injection container is properly constructed from your disparate sets of DI providers. That doesn’t make it an inherently bad thing.

Not saying that Angular itself is a bad thing and of course not saying that zone.js is completely useless. But if I would be forced to start a project on Angular, these days I’d go with a reactive store, for example using @ngrx/store + @ngrx/component-store, onPush change detection, and fully disabled zone.js thing.

Do your thing, but I personally am not a fan of using reactive stores or ngrx. Service with a Subject is more than enough when used properly, and is a more composable model than dictating a central data store for the entire application. The complexity introduced by reactive store models is far from free and time travel debugging isn’t enough for me to overhaul the entire application architecture.

@rvalimaki. To summarize.

  1. Zone proposal is not updated for a long time.
  2. We may expect Chrome team release PromiseHook in Javascript like nodejs, but it is a very very long shot.
  3. With Angular Ivy in the future, there will be a non zone mode, and user need to trigger change detection themselves.

The async_hooks version of zone.js is still developing, will update the status when I have some progress.

Just want to point out the “obvious” workaround for Node.js, which is to downgrade target to ES2015 😄 . Thanks for your great work!

You are always free to use a different change detection strategy other than Zone.js. Personally for Angular I use (and prefer) Zone.js by default and use Zone escape (via NgZone#runOutsideAngular()) for high performance timers and optimizations. It seems like a lot of people suggest removing Zone.js from their Angular project without communicating the development costs that will be associated with that change: Either committing to reactive state management or manually triggering change detection like in the Angular.js days. These are high cost alternatives for applications which often will not see tangible benefits from removing the overhead of Zone.js.

It’s fine if you as a developer want to do that, but it’s never as easy as “just remove Zone.js” 😃

More importantly, and separately from the debate about state management, Zone.js solves use cases that are impossible, leaky or difficult without it, which have little or nothing to do with Angular:

  • It can be used (outside Angular) in testing frameworks to track test case execution without the need for done(), avoiding false positives where a done() call, await, or promise return is missed. This is how the Razmin testing framework handles asynchronous tests (disclosure: it’s my library).
  • It can be used to attribute log messages to execution contexts (ie, stamping the UUID of an HTTP request onto a log, or noting which background task started the thread of execution)
  • It is used (fully transparently) in SSR to detect when it is safe and correct to send the HTML back to the browser, because all pending asynchronous tasks have been completed. Note that this has nothing to do with your choice of change detection strategy.
  • It is used to intercept global APIs in specific contexts, even while other code outside the context uses the original/unaffected global APIs
  • It is used to know when a transaction can be automatically closed

These are just the use cases I myself have used it for over the last few years. There are countless more that have yet to be thought of. Zone.js is a fantastic library, and I can only hope that it can make the leap to supporting async/await so we can enjoy it until the ES committee can be persuaded to adopt support for something like it into the language and the runtime.

All this is not to say that the library itself does not have rough edges, or that there aren’t paths for improvements.

  • The Zone API is complicated and difficult to understand
  • Zones (even with zone-aware stack traces) can hamper debuggability quite a bit
  • Zones do have a performance impact, though I think a lot of people think the performance impact they see in Angular is because of Zone, but in reality Zone is just telling Angular when something has completed, it is then Angular which triggers a change detection. This change detection is where a lot of the observed overhead comes from
  • And of course, Zones cannot patch native async/await, the whole reason we’re here.

We would also like updates on this. Is there any work being done to address this incompatibility?

It would be fine though, in my opinion, to use it for prototype-like projects (so should be disabled by default).

I think it should be the default, but I wouldn’t be too upset if it were made not to be – I’d hope Angular continues to offer it as an option regardless, but even if they don’t, it is actually quite trivial to implement Zone based change detection without Angular’s help so I would probably add it back in and publish it as an addon package for those who want to use it 😃

Service with a Subject is more than enough when used properly

Ngrx uses subjects internally, behavior-like one I think. The point here is unification, so you jump between projects in a productive manner since there is no need to first dive into the fancy custom built on subjects store management library. Ngrx, btw, was named as an example.

So to be clear, I’m not talking about building a store based on Subject. I’m talking about building services which expose Observables. “Service with a Subject” is just the popular vernacular for the approach, which stands in contrast to the recent uptick in interest in React style data management strategies like Redux, Flux, Ngrx, or other reactive stores. “Service with a Subject” isn’t really a replacement for a reactive store itself, it is just a way of organizing your app’s data flow, which almost any Angular developer is probably already familiar with, even if they didn’t know that there was a term for it.

The canonical example is a UserService that tracks the current user:

@Injectable()
export class UserService {
    private _userChanged = new BehaviorSubject<User>(null);
    get userChanged() {
        return this._userChanged;
    }

    // ....
}

A component interested in the current user would subscribe:

@Component()
export class LoginStatusComponent implements OnInit, OnDestroy {
    constructor(
        private userService : UserService
    ) { 
    }

    private subsink = new SubSink();
    user : User;

    ngOnInit() {
        this.subsink.add(this.userService.userChanged.subscribe(user => this.user = user));
    }

    ngOnDestroy() {
        this.subsink.unsubscribe();
    }
}

I imagine there are a great many of these UserService (or AuthService, AccountService, LoginService, whatever you decide to call it) within the Angular apps out in production right now, using this sort of model.

But you can use this for everything- even for “instance” style subscriptions. Say you have a ChatRoom class that represents an incoming stream of chat messages in a particular “room”? ChatService#getRoom(roomID) could return a ChatRoom class instance which has a messageReceived observable, which you then subscribe to.

This really doesn’t have anything to do with change detection, but simply data flow in your application. And indeed, reactive store solutions also don’t have anything to do with change detection, except that you can hook into when the state has been definitively updated, and only trigger change detection when that happens.

The point here is unification

I think you mean “conformity” 😃

Do your thing

Thank you. But that was a way to go if I’m forced to start something new on Angular which is not the case.

I guess the difference is I proudly start new apps in Angular to this day. If you would prefer starting an app in a different framework that’s great, there’s nothing wrong with that, but I prefer Angular over the alternatives (though I do enjoy learning about those frameworks and the new concepts arising from them, and I certainly don’t think Angular is the perfect framework).

Nonetheless, thank you @vladimiry for the lively and civil debate about the pros/cons on this topic, and thanks for all subscribers to this issue for entertaining the discussion 😃

@enko, @shellmann, this issue is still pending because there is no proper way to patch async/await promise in Javascript world. In nodejs, it is possible to do the patch and I have made some POC to verify it. But in browser, it is still impossible to do that, so we will wait whether zone will pass TC39 proposal or browser can open some PromiseHook like API to us.

So now, sorry this issue can not be resolved, so Angular can not work with ES2017 currently.

The Zone.js API is overwrought and over complicated.

I agree, that’s why I started modelling an alternative API which solves the same problem-space in a simpler way. Check out https://github.com/rezonant/rezone . The API is much simpler, and might be a more approachable path to standardization. The prototype implementation is built on top of Zone.js for simplicity. However, the design of Zone.js’ API does not invalidate the underlying concepts; we still need a way to observe the lifecycle of chains of asynchronous operations at runtime.

Its carcass was merged into the Angular repo from its original home because no one else had any independent interest in it.

@pauldraper I think this is a bit harsh. You might consider the developers who originally built it and all the challenges they had to solve, and how much value the entire community has extracted from it, even though it seems to be a punching bag amongst the under-informed in the community. Myself and others are posting here because we not only see the potential of what Zones in Javascript can do, but that we are already making use of it in production scenarios today (outside of Angular).

Honestly it’s not surprising that it took years for anyone to realize how to apply Zone.js to other use cases. The concepts themselves are subtle, and as we’ve both pointed out, the library itself (and certainly its documentation) is rough, and the API design is, yes, overcomplicated and formidable. One need only search for “Zone Angular” to see the reams of “What exactly is Zone.js” and “How exactly does Zone.js work” articles which themselves are a bit hand wavey and overly focused on the use case of change detection.

Yes, thank you.

I am using zone.js because it’s a necessary dependency of opentelemetry in the browser. Unfortunate that such a new observability project is so dependent on a fragile and aging approach.

Though I’m not familar with OpenTelemetry’s codebase, I’m familiar with what it is meant to do, and I have no doubt it is using Zone.js specifically for the type of benefits I mention above- it is able to track execution contexts across asynchronous calls. As far as I’m aware, there is no other way to do that other than hotpatching the APIs like Zone.js does.

It’s not that the approach is aging, but simply that the final design of ES’ async/await did not allow for interception as needed for a user-land observability framework like Zone.js.


Trying to veer back on to the task at hand, let me sum up the possibilities for how to solve native async/await in Zone.js generally:

  1. Handle it at compile-time like Angular is now, but in a way that can be enjoyed by non-Angular users like OpenTelemetry, Razmin and Alterior
    • While this isn’t ideal, it seems like the only practical option in the short to medium term.
    • Such a solution would probably be best implemented as both a Babel plugin and a Typescript plugin for the best developer ergonomics
    • It would have to be used in the build process by the end developer to ensure that all native async/await usages (across packages) are instrumented
    • Ideally would instrument, not down-level – If we’re going to go through the work to implement it, we might as well take advantage of the reduced overhead and platform integration that runtime-supported async/await offers.
  2. Petition the TS developers for a reprieve: The ability to down-level async/await while targetting higher ES versions. I think this will be difficult to accomplish, and does nothing to solve the issue for non-TS users
    • Furthermore, TS does not modify code used by packages anyway (except possibly when bundling?). When consuming a package that targets ES2017+, it won’t matter that you target ES2016 because the library’s async/await usages remain native, thus causing a Zone leak when that code is executed.
  3. Champion a proposal to ES to introduce an observability API (similar to async_hooks) that could be made available in all ES runtimes.
  4. Champion a proposal to bring a Zone or Execution-Context primitive into ES itself. This has already been attempted by Domenic Denicola prior but was unsuccessful. My rezone repository attempts to reimagine that effort

For anyone new to the issue: Support for native async/await can’t be trivially implemented because await does not trigger the creation of Promise like the down-leveled awaiter-style system used by Typescript and Babel does. Because no instance of Promise is created, Zone.js’ own ZoneAwarePromise cannot feed back task information into the current zone, thus any execution context which is resuming after the native await will fall outside of the Zone they were originally created in. To put it simply, there is no place for Zone to “hook”.

Node.js’ async_hooks (not supported in the browser, nor Deno, nor other ES runtimes) is part of the solution, but the implementation work has been lagging behind for quite some time.

EDIT: Edited to add note that TS downleveling can’t solve for libraries

You can use Angular with any version of ES the only thing you need is async/await downleveling.

Just pipe babel with async/await plugin into AngularCLI underlaying webpack (using @angular-builders/custom-webpack)

@thekip could you demonstrate this in a small code example ?

Thanks.

Angular is working on making the zone.js dependency optional

Awesome that it’s being worked on. If zone.js can’t be patched, this ticket can still be resolved by shipping the zoneless alternative, with a clear migration path, thus making this issue actionable.

This 7 year old ticket, influencing things as important as TC39 discussions, really should only be closed with the release of that new feature. If it’s to stay closed anyway, without action, I’d ask if it could be done so as “not planned” rather than “completed”, because many are tracking this ticket.

Where will the new “zoneless” mode of operation be tracked going forward?

Why so negative?

There is a lot of why, here is just a short list:

  • It’s complex, adds weight, requires to be maintained.
  • It’s a monkey patching. So by definition not a forward-looking idea. It might potentially make the patched functions work differently in comparison with native/original behavior and also might affect the performance. Async/await is/was the first blocker, there might be more in the future.
  • There is no such thing as an interface put on top of implementation. So if you started using it from day one there won’t be a simple way to replace the change detection by changing the interface implementation without a need to rethink and rewrite a huge part of the code base. No interface and so no simple/static/type-safe way to detect all the places where the change detection is being triggered. So you got Angular built on DI and other enterprise-grade fancy ideas but here you left with just that “implicit magic”.
  • It’s enabled by default and so most of the devs likely just go with it without a doubt.
  • It hides how things work from those who just go with defaults.

Not saying that Angular itself is a bad thing and of course not saying that zone.js is completely useless. But if I would be forced to start a project on Angular, these days I’d go with a reactive store, for example using @ngrx/store + @ngrx/component-store, onPush change detection, and fully disabled zone.js thing.

@rezonant those are some noble ideas, but Zone.js is all but dead. It doesn’t even work with Node.js async/await, despite async_hooks being available.

The Zone.js API is overwrought and over complicated.

Its carcass was merged into the Angular repo from its original home because no one else had any independent interest in it.

So does Angular still not support ES >ES2015 at all? If not, I really it becomes possible to at least use those newer versions of JavaScript, even if native async/await can’t be supported.

Some thoughts after a few months of transpiling async-to-generators to make Zone work. Definitely the call stacks are much less helpful, at least that’s the case in jest tests.

Therefore, I am still wondering if it was possible to do a better Babel transformation, which would keep native async/await instructions, but wrap them in the necessary zone-related decorator logic? Personally, I would be fine seeing a couple of additional levels per await call in my call stacks, it’s just that with generators the async/await logical stack seems to be lost altogether.

image

Here is a screencast of the debugging experience with the transform and Zone.__awaiter function.

https://user-images.githubusercontent.com/15655/125838981-e30398a1-aae0-462d-a267-3ca88023faba.mov

Adding my two cents here as a non-Angular developer - to disprove a claim that no one is really interested in zones outside of Angular.

I find zone.js extremely useful as my app has a SharedWorker servicing multiple browser tabs with different contexts.

Many of the services I develop are “context-isomorphic”, namely, they can work both in one of the main contexts, or in the worker.

Within a worker, the necessary context information is stored via a zone fork as soon as I receive an RPC message to process.

Then, down the async call stack I can always do something like Zone.current.get('context') ?? getSingleContextForMain(), allowing me to build worker+main isomorphic services.

While the problem outlined here has not yet become relevant for me (I have not even yet switched from ES5 to ES2015, haha), I would like to cheer up for the team’s efforts in resolving this and pushing for standardised Zone support in JavaScript.

I also appreciate the hard work the team has invested in this so far. Please don’t abandon this.

I believe Angular 11.2 will have support for ES2017+ when it’s released if all goes well, so you can start targeting that ES level with that version and still use zonejs. If you want to opt out of automatic change detection, then you need to handle it via some other means or do it manually. Look up ChangeDetectionStrategy. The options are to use a reactive store approach like ngrx or to manually trigger change detection when you need the view to update. If you already have a sizable app using zonejs, as myself and others have pointed out, it may be expensive to retrofit a reactive store into your app. If you are already on Angular 11 then it’s probably best to adopt 11.2 when it comes out.

hi @rezonant let me add the possibility to let the template manage your ChangeDetection. There is actually a library which provides utilities for this. So you might want to take a look at @rx-angular/template and its approach to ChangeDetection in angular applications. It actually combines really nice with the reactive store approach you mentioned. This way you can push state changes directly to the template, where ChangeDetection will get managed for you.

And if you are btw. searching for a slim reactive state solution, you surely will enjoy @rx-angular/state. Both packages are designed with template-driven ChangeDetection in mind so you get rid of any manual ChangeDetection

@rezonant best bang for buck is probably moving Angular users away from Zone. It’s not really pulling its weight anymore, and keeps us stuck in the past

So are we stuck on this?

@rvalimaki, this is a very difficult issue to resolve, I am still trying to find out a walk around. @Eugeny, current zone.js already monkey-patch Promise, but async/await is different, it does not use javascript Promise, it always use Native Promise.

@JiaLiPassion hi!, any update on this? we are using async/await and doing so expecting leverage native … and we fall into this situation where is not only not native but also a nightmare to debug vs our previous rxjs (nonative pipe hell) approach. appreciate your insights on forward. 🖖

OK so Im still seeing this warning 3 years on in 2020 when I have target es2018 with lib esnext thus supporting es2020 features, Are we saying that since 2017 angular, react, and and anything using zone is redundant?

@mhevery , got it, I will try to use async-hook to implement Zone in node.js and let you review after finish!

I’m going to close this issue as it’s inactionable for us. Zone will never be able to be dropped on a page and be compatible with async/await as the API is fundamentally not patchable. Any integration must depend on compiler transforms, and our CLI uses the technique of downleveling to Promises today.

For this and a variety of other reasons, Angular is working on making the zone.js dependency optional, via a “zoneless” mode of operation. Additionally, #54952 allows change detection to work with native async/await as long as Angular is notified about the change independently (e.g. markForCheck or signals).

@avatsaev: Why so negative? Zone.js support for async/await in ES2017+ within Angular itself is effectively solved as of the commit referenced above. You’re free to use it or opt for some other way to manage change detection using ChangeDetectionStrategy.

Though it’s arguably not the best place to debate the merits of Zone.js, given that we have a lot of work before this issue can be fully closed, I’d welcome your thoughts on why you feel that way, provided Jia Li or any other Angular team members / moderators don’t object, as after all, this is their venue.

I’ve followed this issue for a while now of curiosity and I think it’s time to share my own solution to the problem, implemented in Dexie version 2.0. It has been used for quite a while and works across all browsers keeping the zones between await calls. My first beta was released in October 2016, and the first stable 2.0 release in September 2017. Dexie has ~15k weekly downloads on npm and there are no show-stopping issues related to its zone system.

The reason for having a zone system in Dexie is to keep track of ongoing transactions. First version with the zone system built-in was release in 2014 (but then without support for async/await). This was before I knew about the concept of zones, so I thought it was my own invention at first, and called it PSD (Promise-Specific Data) as it only cares about promise-based async flows. The 2.0 version use something I call zone-echoing. It will enque micro-tasks that will re-enter the zone in coming micro-tasks. It can know when it’s time to stop the zone echoing as long as the user sticks to its own Promises. A fallback will stop it by a limit. This works across all modern browsers and does not leak zones (except for MutationObserver subscriptions, whose events will derive the zone from the zone where the subscription was initialized - which wouldn’t be a problem in angular/zone.js as it also patches those events).

The technique I use could be used in angular/zone.js but then every promise-returning function in the DOM would have to return a zone’s own Promise (maybe it already does?) in order to invoke the zone echoing. There could be performance implications that would need to be considered. Especially as I detect native await calls by defining Promise.prototype.then using a getter instead of a value, and have to echo zones in order to support calls that awaits non-promises.

If interested, the implementation lies in Dexie repo at src/helpers/promise.js.

I regret to say we completely ditched zone.js and opted for explicit pass-down of the context parameter. We experienced nice performance improvements and good call stacks, and overall I’d say it was the right call.

Now we’re tracking this: https://github.com/tc39/proposal-async-context

I think it will solve a lot of problems that we were trying to solve with zone.

@JiaLiPassion I don’t have a “starter” for my setup but it builds with Webpack, using @ngtools/webpack. I could open a new issue over at the angular-cli repo if you think this isn’t going to get resolved soon. Webpack users should definitely be getting warned about this.

@manklu I read it, and I stated I don’t want to pipe anything to babel… I don’t want to have babel in my build pipeline.

What about nasty little transpiling trick so that every “await foo()” is transpiled to “await foo(); app.tick()”? That way async await would be ~native, but with additional code on run time.

Also thinks that this is a good idea, but wrapping foo() into Zone.current.fork(..) would be a better solution. It might be implemented as babel plugin (to target not only typescript users) and added into Zone.js package.

Angular CLI already using Babel under the hood for downleveling es2015 to es5, piping babel into webpack shouldn’t be a big deal and also allows to integrate babel/preset-modules (addressing this issue: https://github.com/angular/angular-cli/issues/16170)

@JiaLiPassion can you say something about the current status?

@birkskyum Define which issue.

Since v15, Angular forces ES2022 but uses babel-plugin-transform-async-to-promises to convert async/await to promises. (zone.js cannot patch await). This is still true even when using the noop-zone cf angular/angular-cli#22191

Just for the record, our non-Angular project using zone.js was able to successfully upgrade to ES2018 with no issues.

I did as @petebacondarwin suggested, we only had to “cherry-pick” the async/await-to-generators Babel transform to our build pipeline and zone.js works like a charm.

Many thanks 💪💪💪

Is there a way to enable native async/await? We have a large Angular 13 project running absolutely fine without zone.js it would be nice if it was possible to remove the transform-async-to-generators from the transpile stage. I’ve removed manually for now and everything works. The debugging experience is very nice in a no zone.js native async/await Angular project. We have our own state framework and can trigger a tick when we need.

In fact depending upon your browsers I think you could even target ES2020…

When considering that, be aware that targeting anything newer than ES2017 will mean object spread is no longer transpiled and there are severe perfomance issues with native object spread in Chrome; see this issue I reported some time ago that hasn’t been fixed so far: https://bugs.chromium.org/p/chromium/issues/detail?id=1152728.

This was causing real issues in our application and we had to settle on transpiling to ES2017 to avoid it, unfortunately.

I’m just aware that there are a bunch of articles from the past few years about “Angular without zone.js”

Like most Angular articles they are long on words and short on substance.

Using Angular without Zone.js is onerous in practice. And you’ll never be able to use any third-party components, like Angular Material.


You have to call ChangeDetectorRef#detectChanges(). Don’t confuse that with ChangeDetectorRef#markForCheck() called by the async pipe and others. The last marks the component and dirty, but depends on Zone.js calling ApplicationRef#tick() to actually trigger the change detection on the tree.

Yes it down levels async/await using babel internally in the angular compiler, so it should be transparent to the app developer. If you are not using angular CLI then you wouldn’t benefit from the chanhe, but then again you always could have put the babel plugin into your custom webpack configuration, so that could be set up in a project today if desired.

The only downsides on that is perhaps less efficiency (since native async/await can benefit from runtime optimizations) but that was already the case on ES2016 target and at least you’ll be able to use newer ES features in your codebase, and libraries using native async/await will be automatically downlevelled as well, so no worries about using libraries that make use of that feature, whereas before it was a subtle foot gun, since you couldn’t control whether a lib used native async/await without a compiler pass.

It’s a nice stopgap to ensure devs can continue to target newer ES versions, and this issue is here to track progress around a more wholistic solution that doesn’t need to downlevel async/await while still ensuring code cannot unintentionally escape from a zone.

I came here to report that running unit tests with target: "es2018" caused the tick() function to fail to wait for await Promise.resolve(...). I tracked it down as far as noticing that under es2015, that line added a task to the zone’s _microtasks but under es2018 it did not.

Am I correct in thinking that my problem has the same root cause as this issue? If so, is there a workaround other than compiling your code down to es2015? I have the luxury of running in a known environment so targeting es2018 has worked well so far… except, I guess, for this.

ETA: Also, I never got the warning referenced in https://github.com/angular/angular/issues/37742 . (“WARNING: Zone.js does not support native async/await in ES2017.”) Does anyone know if that warning is specific to ng CLI? I don’t use the CLI but would certainly have appreciated a warning like that before I spent 2+ days pulling my hair out trying to solve this.

What’s the status of this? I don’t want to pipe anything to babel. I just want to use it in evergreen browsers straight from the ts output.

What about nasty little transpiling trick so that every “await foo()” is transpiled to “await foo(); app.tick()”? That way async await would be ~native, but with additional code on run time.

Good idea, though zones let you do more, like intercept errors and be notified when async tasks start and end, the latter being important for SSR so the server knows when it’s time to send the final HTML.

I’ve been following this issue for awhile and did some preliminary work on a newer proposal that attempts to simplify zones with the hope that a more intuitive paradigm could dispell the naysayers in the Node community that are afraid of domain-like consequences for adopting zones. I have read all the discussions that led to the zone proposal withdrawal but I ended up strengthening zones caller-control by eliminating zone escape entirely. It’s been on the back burner lately as there are some issues in the design and I have a lot of other projects, but perhaps it could be an inspiration or starting point for someone to push it forward. More details can be found at https://github.com/rezonant/rezone

@rvalimaki

… and probably better than the current situation.

Much better than transpiling all modern ES features.

With Angular Ivy in the future, there will be a non zone mode, and user need to trigger change detection themselves.

Any examples of this?

@pauldraper Actually, if I am not wrong, there was a talk on the subject at AngularConnect. One of the suggestions is to get closer from what does React to trigger DOM updates.

See https://www.youtube.com/watch?v=rz-rcaGXhGk

@dfahlander, thanks for your post again, I have sent you an email, please check it.

What you’re looking for is being tracked at angular/angular-cli#22191. It’s not possible at the moment.

@JeanMeche Interesting. For those of us who are okay with zone.js not being able to patch await, is there a way to disable the babel-plugin-transform-async-to-promises entirely and ship async/await in the bundle to the browser?

Now we’re tracking this: https://github.com/tc39/proposal-async-context

I think it will solve a lot of problems that we were trying to solve with zone.

For Angular, the “Zone problem” is being solved by Signals

I wonder if there is a way to support this with some unplugin/babel/webpack plugin which will transform the code to propagate the zone? Instead of relying on Promise being patched, the expression inside await can be wrapped with a function which will propagate the zone?

@thduttonuk - the transform that you mention is added by the CLI not the core framework. Please open a feature request at https://github.com/angular/angular-cli/issues

In fact depending upon your browsers I think you could even target ES2020…

It seems the Zones TC39 proposal isn’t happening anymore as it’s listed under inactive proposals with rationale “Withdrawn; champion is no longer participating in TC39”

There is an out of the box solution for projects built with Angular compiler. If your end project (ie the backend you are writing) happens to be using a Babel build step, you can add a Babel plugin to downlevel async/await. If your end project does not have a Babel build step (or if you are producing a library depending on zone.js) you will need to target ES2016 in all relevant dependencies to ensure that native async/await is not used. If you are authoring an end project (ie a backend itself, not a library) and you need ES2017+ features without transpiration you would need to add a build step to downlevel async/await.

Personally I author (backend) frameworks and libraries which use zone.js so I’m in the most difficult boat, but I have not yet been burned by targeting ES2016 in those projects and my own backends using them. It’s only a matter of time before dependencies built using ES2017 become a problem and lead to unexpected zone escapes though 😕

I believe Angular 11.2 will have support for ES2017+ when it’s released if all goes well, so you can start targeting that ES level with that version and still use zonejs. If you want to opt out of automatic change detection, then you need to handle it via some other means or do it manually. Look up ChangeDetectionStrategy. The options are to use a reactive store approach like ngrx or to manually trigger change detection when you need the view to update. If you already have a sizable app using zonejs, as myself and others have pointed out, it may be expensive to retrofit a reactive store into your app. If you are already on Angular 11 then it’s probably best to adopt 11.2 when it comes out.

The zone concept still has significant potential. The problem is that it has been expected its native implementation in browsers spec.

That’s great news, but I do hope the team will keep exploring ways to enable this outside of angular. I use zonejs outside of angular for many purposes

I strongly agree.

One option to consider is using ahead of time compilation to decorate all async operations with needed state preservation idioms. This approach would assume of course that you aren’t doing any runtime loading of uninstrumented code.

That’s great news, but I do hope the team will keep exploring ways to enable this outside of angular. I use zonejs outside of angular for many purposes

I don’t mind if babel is used for transpiling, but maybe not as it seems to be pretty heavy weight solution for that. But it’s OK as long as everything is done under the hood and I can just set the ES2019 or whatever target.

Piping babel by hand per project is no-go. Also if in the future the real solution for async await emerges, any custom hacks for projects have to be removed.

@JiaLiPassion when will it ready, as I currently only need it in nodejs;