# zone.js
# What the Heck Is zone.js?
zone.js is merely a library that implements the TC39 proposal, which was ultimately withdrawn. zone.js primarily monkey-patches the entire asynchronous browser API, including DOM timers, Promises, and EventTarget.addEventListener.
The original intention of this proposal was to track asynchronous jobs within a specific zone. It was initially designed to be implemented natively within browser engines and was later decoupled from Angular.
However, Angular relies on zone.js to determine whether or not to run change detection when any asynchronous task is triggered within the Angular zone.
Let's look at the following example to understand how zone.js patches API:
// Save the original reference
const originalRequestAnimationFrame = window.requestAnimationFrame;
// Overwrite the API with a function that wraps provided callback
window.requestAnimationFrame = function (callback) {
const wrappedCallback = Zone.current.wrap(callback);
return originalRequestAnimationFrame.call(this, wrappedCallback);
};
Zone.current.wrap = function (callback) {
// First we capture zone before the callback is scheduled
// and invoked in the future
const capturedZone = this;
// Return a closure which executes the callback in the captured zone
return function () {
return capturedZone.run(callback, this, arguments);
};
};
How does Angular use zone.js?

The internal flow:

# Dealing With zone.js
There isn't much that developers need to know from their perspective. A high-level overview is sufficient because developers never directly interact with the zone API.
When debugging an issue and observing unusual behavior related to change detection, it's always helpful to use the // @ts-ignore console.log(Zone.current) command. If it logs angular, everything is fine, and your component doesn't have a ChecksEnabled state (meaning it is not marked for checking). In such cases, the next step is to invoke detectChanges().
If it logs <root>, it means that the callback was either added or invoked inside the root zone.
In simple terms, it's important to understand that zone.js deals with callbacks, where everything is considered a callback:
(click)="handleClick()"is a callback passed toaddEventListenersubscribe(function handleNext() {})is a callback passed toObservableorSubject(stored differently by each)
The execution context is lost at the point where these callbacks are invoked. The execution context refers to a single variable, and you can examine it by running the following:
Object.getOwnPropertyDescriptor(Zone, 'current').get.toString();
Each time the run method is invoked on an instance of the Zone class, zone.js internally reassigns the variable:
import 'zone.js';
const zone = Zone.current.fork({
name: 'angular',
});
zone.run(() => {
setTimeout(() => {
// ...
});
});
What run does internally is:
let _currentZoneFrame;
class Zone {
static get current() {
return _currentZoneFrame.zone;
}
run(callback) {
_currentZoneFrame = { parent: _currentZoneFrame, zone: this };
try {
return this.delegate.invoke(callback);
} finally {
// Restore the execution context back
_currentZoneFrame = _currentZoneFrame.parent;
}
}
}
Event listeners also retain the execution context in which they were added:
import 'zone.js';
const zone = Zone.current.fork({
name: 'angular',
});
const div = document.createElement('div');
zone.run(() => {
div.addEventListener('click', () => {
console.log(Zone.current); // will log angular
});
});
document.body.appendChild(div);
Zone.root.run(() => {
div.click();
});
Most objects can be treated as an EventTarget (except for promises and some new browser APIs like navigator.geolocation, etc.), and it is sufficient for zone.js to patch the EventTarget.prototype.addEventListener method. This is because other classes extend EventTarget and will inherit the patched behavior through the "super" method.
When addEventListener is called, it stores a ZoneTask on the element. The ZoneTask is created to retain the Zone.current at the time of addEventListener invocation. The ZoneTask includes the _zone property, which references the zone in which addEventListener was invoked. This _zone will be reused in the future. You can inspect the event listeners on the element and the corresponding execution context that was captured.:
// These functions only work when you call them from the Chrome DevTools
// Select the element in elements panel
element = $0;
allEventListeners = getEventListeners(element);
Object.keys(allEventListeners).forEach(eventName => {
const capturable = element[Zone.__symbol__(eventName) + 'true'] || [];
const nonCapturable = element[Zone.__symbol__(eventName) + 'false'] || [];
tasks = [...capturable, ...nonCapturable];
tasks.forEach(task => {
console.log(
'callback = ',
task.callback,
` for ${eventName}, was added when zone was ${task._zone.name}`,
);
});
});
# zone.js and Server-Side Rendering
Angular's server-side rendering works only thanks to zone.js for now. zone.js keeps track of all the asynchronous tasks being spawned within the Angular zone and forbids serializing the DOM tree into a final HTML until all these tasks are completed. Consider the following example:
@Component({
selector: 'app-root',
template: `
<p>{{ microTask }}</p>
<p>{{ macroTask }}</p>
<p>{{ eventTask }}</p>
`,
standalone: true,
})
export class AppComponent implements AfterViewInit {
microTask = '';
macroTask = '';
eventTask = '';
private readonly http = inject(HttpClient);
ngAfterViewInit(): void {
Promise.resolve().then(() => {
this.microTask = 'microTask';
});
Promise.resolve().then(() => {
setTimeout(() => {
this.macroTask = 'macroTask';
}, 3000);
});
this.http.get('https://jsonplaceholder.typicode.com/todos/1').subscribe(() => {
this.eventTask = 'eventTask';
});
}
}
If we build the following code, run the server, and then execute the curl command, we will notice that the response does not return immediately. Instead, it takes some time until all the tasks are completed (Promise.resolve().then, Node timer, and HTTP request):
$ curl localhost:4200
...
<app-root ng-version="..." ng-server-context="ssr"><p>microTask</p><p>macroTask</p><p>eventTask</p></app-root>
...
Angular utilizes isStable, which emits true when there are no pending micro and macrotasks within the Angular zone. It's important to note that tasks scheduled within the runOutsideAngular method do not impact server-side rendering, as the HTML may be serialized before those tasks are completed.
async function _render(platformRef: PlatformRef, applicationRef: ApplicationRef): Promise<string> {
const environmentInjector = applicationRef.injector;
// Block until application is stable.
await applicationRef.isStable.pipe(first((isStable: boolean) => isStable)).toPromise();
}