In a zone-based Angular app, how do NgZone.runOutsideAngular() and NgZone.run() work together, and what breaks if you forget to re-enter?
answer
- the zone is inherited by callbacks
- leave for the noisy work
- come back for the one update
- isInAngularZone tells you where
basics
~20 sCode started in runOutsideAngular(), and every callback it schedules, runs outside the Angular zone and triggers no checks. run() re-enters for the update that matters; forget it and a plain-field update, even in a parent's output handler, never renders.
solid answer
~50 sA zone is inherited: timers, listeners and promises created inside `runOutsideAngular()` keep running outside the Angular zone for their whole life. That is the point, since the noisy work stops triggering checks, but it also means a callback that must update the UI is outside too. `NgZone.run(fn)` re-enters: zone.js treats `fn` as a task inside the Angular zone and Angular checks when it finishes. Forget it, and a callback that assigns a plain field, or emits an `output()` whose parent handler assigns one, changes state with no check, so the screen stays stale until something unrelated triggers one. `NgZone.isInAngularZone()` tells you where code is running. Since v18, zone-based apps also schedule a check when a template-read signal is set or `markForCheck()` is called outside the zone, so a signal write is an alternative to `run()`. Keep `run()` blocks small.
code
ts · 26 linesimport {Component, ElementRef, NgZone, afterNextRender, inject, output, viewChild} from '@angular/core';
interface EditorHandle { on(event: 'save', cb: (doc: string) => void): void; }
declare function createEditor(host: HTMLElement): EditorHandle;
@Component({
selector: 'app-note-editor',
template: `<div #host></div>`,
})
export class NoteEditor {
private readonly zone = inject(NgZone);
private readonly host = viewChild.required<ElementRef<HTMLElement>>('host');
readonly saved = output<string>();
constructor() {
afterNextRender(() => {
this.zone.runOutsideAngular(() => {
const editor = createEditor(this.host().nativeElement);
editor.on('save', (doc) => {
console.log('in Angular zone?', NgZone.isInAngularZone()); // false
this.zone.run(() => this.saved.emit(doc));
});
});
});
}
}go deeper
Know that runOutsideAngular() stops Angular reacting to some work and run() brings code back so the UI updates.
Explain zone inheritance, the leave-then-re-enter pattern, and why a forgotten run() leaves plain-field state stale.
Keep re-entry narrow, trace outputs and HTTP calls that inherit the outside zone, and prefer signal writes where they are clearer.
Set library-wrapping conventions that work in both zone-based and zoneless apps, so wrappers do not need rewriting during a migration.
## Zones are inherited In a zone-based Angular app, zone.js propagates the **current zone** to everything scheduled from it. A `setTimeout` created inside the Angular zone calls back inside the Angular zone; an event listener added inside it fires inside it. `NgZone` exposes two methods that move code between zones: - **`runOutsideAngular(fn)`** runs `fn` in the Angular zone's parent. Everything `fn` schedules (timers, listeners, promise continuations, library-internal tasks) is also outside, for as long as it lives. - **`run(fn)`** runs `fn` inside the Angular zone. When it finishes, zone.js reports a completed task and Angular runs change detection. The static method **`NgZone.isInAngularZone()`** returns whether the current code is inside the Angular zone, which is the quickest way to check an assumption while debugging. ## The pattern The Angular performance guide's recipe for third-party libraries is: 1. Initialise the library inside `runOutsideAngular()`, so its internal timers and listeners stop triggering checks. 2. Subscribe to the few library events the application cares about. 3. Inside those handlers, wrap the state update in `run()` so Angular checks once. ```ts this.zone.runOutsideAngular(() => { const editor = createEditor(this.host().nativeElement); editor.on('save', (doc) => { this.zone.run(() => this.saved.emit(doc)); // re-enter for the one event that matters }); }); ``` ## What breaks if you forget `run()` The library handler above runs outside the Angular zone because it was registered there. Without `run()`: | Handler does | Result in a zone-based app | |---|---| | Assigns a plain field read by a template | No check is triggered; the screen stays stale | | Emits an `output()` whose parent handler assigns a plain field | The parent handler also runs outside the zone; the parent stays stale | | Calls a service that later makes an `HttpClient` request | The request runs outside the zone too; a plain-field update in its response triggers no check | | Sets a signal read by a template | A check is scheduled anyway (hybrid scheduling, since v18) | | Calls `ChangeDetectorRef.markForCheck()` | A check is scheduled anyway (since v18) | The symptom is typical: the value appears only after the user clicks something else, because that click triggers a check from inside the zone. It looks intermittent, but it is deterministic: nothing notified Angular. ## Signals as the alternative Since v18, a zone-based app's scheduler also listens for Angular's own notifications that come from **outside** the zone: setting a signal that a template reads, or calling `markForCheck()`. So a handler that does `this.selection.set(doc)` renders without `run()`. That is often cleaner: - it expresses what changed rather than where code runs; - it keeps working unchanged if the app later goes zoneless, where `run()` does nothing; - the check it schedules is targeted: views that read the signal are refreshed, rather than every `Eager` view from the root. `run()` is still the right tool when the code inside must run in the Angular zone for other reasons, for example to let zone-based libraries or `HttpClient` calls started from it be tracked. ## Common mistakes 1. **Re-entering too early.** Calling `run()` around the library's initialisation instead of around one callback puts everything back inside the zone. 2. **Assuming outputs are safe.** An `output()` emitted outside the zone runs the parent's handler outside the zone as well; the parent's plain fields stay stale. 3. **Forgetting inherited async work.** A promise chain or an RxJS subscription started from an outside callback continues outside, so a later `.then()` that updates state also needs re-entry or a signal. 4. **Debugging by guesswork.** Logging `NgZone.isInAngularZone()` at the suspicious line answers the question in seconds. ## Keep the re-entry narrow - Wrap only the state update, not the whole handler; heavy work inside `run()` makes the zone treat it all as Angular work. - Do not wrap high-frequency events such as `mousemove` in `run()` unconditionally, or you recreate the pollution you removed. Re-enter only when the value actually changes, or throttle first. - Leave `runOutsideAngular()` and `run()` in place when moving to zoneless: they become plain function calls and the zoneless guide says removing them can regress zone-based consumers.
- In an Angular zone-based app, why can an output emitted from a runOutsideAngular() callback leave the parent stale?Emitting an output calls the parent's handler synchronously, in the same zone as the emitter. If the emitter runs outside the Angular zone, so does the parent's handler, and a plain-field assignment there triggers no check. Wrapping the emit in `NgZone.run()` puts the handler inside the zone; making the parent's state a signal also works since v18.
- How do you verify at runtime whether Angular code is running inside the Angular zone?Call the static `NgZone.isInAngularZone()`; it returns `true` inside the Angular zone and `false` outside it. Logging it inside a third-party callback is the quickest way to confirm that the handler inherited the outside zone from where the library was initialised.
saying these in an interview costs you the question
- Only the function passed to runOutsideAngular() runs outside the zone.
- Callbacks registered outside the zone return to it automatically.
- Output handlers always run inside the Angular zone.
- Wrap every high-frequency event handler in NgZone.run() to be safe.
- In a zone-based app, only NgZone.run() can trigger a check from outside the zone.