Zones for JavaScript
Zone.js implements the 'Zone' concept for JavaScript, inspired by Dart, providing an execution context that persists across synchronous and asynchronous tasks. It acts like thread-local storage for JavaScript VMs, allowing developers to track context (like request IDs or user data) across complex asynchronous operations. Zone.js achieves this by monkey-patching most standard web APIs (e.g., DOM events, `XMLHttpRequest`, `setTimeout`, `Promise`) and Node.js APIs (`EventEmitter`, `fs`). The current stable version is `0.16.1`, although the project's recent releases align with Angular's major versions (e.g., `v21.2.9`). While historically a core part of Angular's change detection, Angular is transitioning to a zoneless model, using Signals for reactivity. Consequently, Zone.js is no longer accepting new features or low-priority bug fixes, with critical fixes limited to Angular's direct use cases, and its use outside Angular is strongly discouraged.
Common errors
-
TypeError: Object doesn't support property or method 'Symbol.iterator'
cause This error typically occurs in Internet Explorer 11 after upgrading Zone.js to `v0.11.1` or later, due to the default import now loading the ES2015 (ESM) bundle which uses modern JavaScript features unsupported by IE11.fixChange the Zone.js import from `import 'zone.js';` to `import 'zone.js/dist/zone';` in your polyfills or main entry file to explicitly load the ES5 UMD bundle for IE11 compatibility. -
ReferenceError: Zone is not defined
cause This usually means Zone.js was not imported or loaded correctly before its global `Zone` object was accessed. This can happen if the import path is incorrect, or if `Zone.js` is loaded too late in the application bootstrap process.fixEnsure `import 'zone.js';` (or `import 'zone.js/dist/zone';` for legacy) is at the very top of your application's entry point or polyfills file, before any other application code or frameworks that rely on Zone context. -
Maximum call stack size exceeded (or similar recursion error)
cause This can occur when Zone.js's patching mechanism leads to infinite recursion, often due to improperly wrapping or monkey-patching APIs that Zone.js itself has already patched, or when certain libraries interfere with Zone.js's internal hooks.fixReview the load order of libraries that perform global monkey-patching. Ensure Zone.js is loaded first. If using custom Zone hooks (`onHandleError`, `onInvokeTask`, etc.), ensure they correctly delegate to `parentZoneDelegate` to avoid infinite loops. For specific issues, check the Zone.js GitHub issues for known incompatibilities with other libraries.
Warnings
- breaking Starting with Zone.js `v0.11.1`, the default import `import 'zone.js';` now loads the ES2015 (ESM) bundle instead of the ES5 UMD bundle. This change can break applications in legacy environments, particularly IE11, which relies on ES5.
- deprecated Zone.js is no longer actively accepting new features or low-priority bug fixes, and its use outside of Angular application contexts is strongly discouraged. Angular is moving towards a zoneless application development model with Signals, reducing its dependency on Zone.js for change detection.
- gotcha When integrating Zone.js with certain third-party libraries (e.g., `newrelic`, `async-listener`, `continuation-local-storage`), the import order matters. These libraries may patch global objects like `Promise`, `setTimeout`, or `setInterval` before Zone.js, leading to unexpected behavior if Zone.js patches are applied afterwards.
- gotcha Several versions of Zone.js, particularly those aligned with recent Angular releases, have included security fixes related to Server-Side Request Forgery (SSRF) bypasses and Content Security Policy (CSP) nonce issues. Failing to update may leave applications vulnerable.
Install
-
npm install zone.js -
yarn add zone.js -
pnpm add zone.js
Imports
- Zone
import { Zone } from 'zone.js';import 'zone.js';
- Zone
import 'zone.js';
import 'zone.js/dist/zone';
- Zone
const Zone = require('zone.js');require('zone.js');
Quickstart
import 'zone.js'; // Ensure zone.js is loaded to patch global APIs
// The global Zone object is now available
console.log(`Initial Root Zone Name: ${Zone.current.name}`);
// Create a new zone with custom properties
const requestScopedZone = Zone.current.fork({
name: 'requestZone',
properties: {
requestId: 'req-' + Math.random().toString(36).substring(2, 9)
}
});
console.log(`
Starting a new request simulation...`);
// Run a synchronous task within the new zone
requestScopedZone.run(() => {
const currentRequestId = Zone.current.get('requestId');
console.log(` Inside requestZone (sync): Request ID = ${currentRequestId}`);
// Schedule an asynchronous task. Zone.js patches setTimeout to preserve context.
setTimeout(() => {
// This callback will automatically run within 'requestScopedZone'
console.log(` Inside requestZone (async setTimeout): Request ID = ${Zone.current.get('requestId')}`);
if (Zone.current.get('requestId') === currentRequestId) {
console.log(' Async context correctly preserved!');
}
}, 50);
// Another async operation (e.g., a Promise)
Promise.resolve().then(() => {
console.log(` Inside requestZone (async Promise): Request ID = ${Zone.current.get('requestId')}`);
});
});
console.log(`
Back in Root Zone: Current Zone Name = ${Zone.current.name}`);
// Demonstrating another independent zone
const userContextZone = Zone.current.fork({
name: 'userContextZone',
properties: {
userId: 'user-' + Math.floor(Math.random() * 1000)
}
});
userContextZone.run(() => {
console.log(`
Inside userContextZone: User ID = ${Zone.current.get('userId')}`);
setTimeout(() => {
console.log(` Inside userContextZone (async): User ID = ${Zone.current.get('userId')}`);
}, 20);
});