Resolving NG0506: Angular hydration expected the ApplicationRef.isStable() to emit `true` Problems

Why are you so unstable?!

Saninn G. Salas Díaz

Angular Hydration

Angular Hydration is a pretty nice feature that allows Angular applications to be created on the server and then "hydrated" on the client side without any layout shifts. That said, it is not without its issues. One of the most common problems we face when using Angular Hydration is the NG0506 Error, which occurs when Angular expects the ApplicationRef.isStable() to emit true but it doesn't, or doesn't do it on time.

While Angular has a documentation page that explains the NG0506 Error and points to some possible causes (macro tasks open), it does not have a concise way to find those problems.

This problem would theoretically be part of the past when Angular goes full Zoneless, but for now a lot of applications are still using Zone.js, and the NG0506 Error is still a common issue.

How to solve the NG0506 error?

Step 1: Find the open Macrotasks

Finding the origin of the NG0506 Error requires a bit of detective work, we should find all the places where we are setting macrotasks, which is not a funny task in big enterprise applications, plus, sometimes we are not the one creating those tasks but some of the dependencies of the application are doing it in the background.

A better way to do it is to patch ourselves into the Zone task tracking, for this we can create a little function helper that will log the current open tasks. Let's create a file `q-track-ngzone-tasks.ts

There we will define a couple of interfaces, one to extend the Window interface and another one to define the ZoneTask interface (since it is not exported by Zone.js). We will use the extended Window interface to add a qZoneTracker property that will hold our task tracking functions so we can call them from the console.

Also an important step is to import the task-tracking.js file from Zone.js in order to use the TaskTrackingZone. This file is not loaded by default, so we need to add it to our project (and not forget to remove it afterwards).

import '.../PATH/TO/node_modules/zone.js/fesm2015/task-tracking';

declare interface MyWindow extends Window {
  ngRef?: ApplicationRef;
  qZoneTracker?: {
    track: () => void;
    printAllSources: () => void;
  };
}

interface ZoneTask {
  callback: Function;
  cancelFn: Function;
  consoleTask?: { run: Function };
  creationLocation?: Error;
  data?: Record<string, any>;
  invoke: Function;
  runCount: number;
  scheduleFn: Function;
  source: string;
  type: 'microTask' | 'macroTask' | 'eventTask';
  _state: string;
  _zone: any;
  _zoneDelegates?: any[];
}

Now we can create the implementation. We will export on function called qTrackNgZoneTasks that will take an ApplicationRef as a parameter. This function will log the current open tasks once and define our Window property. Note: We are using @saninn/logger as our logger so we can filter logs easily in the console, but you can use any logger you prefer.

export function qTrackNgZoneTasks(appRef: ApplicationRef): void {
  const logger = new SaninnLogger('QTrackNgZoneTasks');
  logger.warn('Tracking NgZone tasks...');

  const myWindow = window as MyWindow;

  const ngZone = appRef.injector.get(NgZone);
  track(logger, ngZone);
  myWindow['qZoneTracker'] = {
    track: () => track(logger, ngZone),
    printAllSources: () => printAllSources(logger, ngZone),
  };
}

The getTask function will be used to retrieve the current open tasks list from the TaskTrackingZone. It will throw an error if the TaskTrackingZone is not found, which can happen if the task-tracking.js file is not loaded. (more on this later).

function getTasks(logger: SaninnLogger, ngZone: NgZone): ZoneTask[] {
  const taskTrackingZone = (<any>ngZone)._inner.getZoneWith('TaskTrackingZone');
  if (!taskTrackingZone) {
    throw new Error(
      "'TaskTrackingZone' zone not found! Have you loaded 'node_modules/zone.js/dist/task-tracking.js'?"
    );
  }
  const tasks: ZoneTask[] =
    taskTrackingZone._properties.TaskTrackingZone.getTasksFor('macroTask');
  if (!tasks.length) {
    logger.warn('ZONE no pending tasks');
  }

  return tasks;
}

Now we define our API functions.

function track(logger: SaninnLogger, ngZone: NgZone): void {
  const tasks = getTasks(logger, ngZone);

  if (tasks.length > 0) {
    logger.warn('ZONE pending tasks=', tasks);
  }
}

function printAllSources(logger: SaninnLogger, ngZone: NgZone): void {
  const tasks = getTasks(logger, ngZone);
  tasks.forEach((task) => {
    logger.log(
      `${task.type} ${task._state} ${task.source}`,
      task.creationLocation
    );
  });
}

TLDR give me the full code of q-track-ngzone-tasks.ts!

Expand me
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/ban-types */
/* eslint-disable no-restricted-globals */
import '.../PATH/TO/node_modules/zone.js/fesm2015/task-tracking';
import { ApplicationRef, NgZone } from '@angular/core';
import { SaninnLogger } from '@saninn/logger';

declare interface MyWindow extends Window {
  ngRef?: ApplicationRef;
  qZoneTracker?: {
    track: () => void;
    printAllSources: () => void;
  };
}
interface ZoneTask {
  callback: Function;
  cancelFn: Function;
  consoleTask?: { run: Function };
  creationLocation?: Error;
  data?: Record<string, any>;
  invoke: Function;
  runCount: number;
  scheduleFn: Function;
  source: string;
  type: 'microTask' | 'macroTask' | 'eventTask';
  _state: string;
  _zone: any;
  _zoneDelegates?: any[];
}

export function qTrackNgZoneTasks(appRef: ApplicationRef): void {
  const logger = new SaninnLogger('QTrackNgZoneTasks');
  logger.warn('Tracking NgZone tasks...');

  const myWindow = window as MyWindow;

  const ngZone = appRef.injector.get(NgZone);
  track(logger, ngZone);
  myWindow['qZoneTracker'] = {
    track: () => track(logger, ngZone),
    printAllSources: () => printAllSources(logger, ngZone),
  };
}

function getTasks(logger: SaninnLogger, ngZone: NgZone): ZoneTask[] {
  const taskTrackingZone = (<any>ngZone)._inner.getZoneWith('TaskTrackingZone');
  if (!taskTrackingZone) {
    throw new Error(
     "'TaskTrackingZone' zone not found! Did you forget to add \nimport '.../PATH/TO/node_modules/zone.js/fesm2015/task-tracking';\n???"
    );
  }
  const tasks: ZoneTask[] =
    taskTrackingZone._properties.TaskTrackingZone.getTasksFor('macroTask');
  if (!tasks.length) {
    logger.warn('ZONE no pending tasks');
  }

  return tasks;
}

function track(logger: SaninnLogger, ngZone: NgZone): void {
  const tasks = getTasks(logger, ngZone);

  if (tasks.length > 0) {
    logger.warn('ZONE pending tasks=', tasks);
  }
}

function printAllSources(logger: SaninnLogger, ngZone: NgZone): void {
  const tasks = getTasks(logger, ngZone);
  tasks.forEach((task) => {
    logger.log(
      `${task.type} ${task._state} ${task.source}`,
      task.creationLocation
    );
  });
}

FINALLY we can start our server (with SSR) there we can open the console and filter by QTrackNgZoneTasks (if using @saninn/logger) and you will see something like this:

Oh My God So Many Tasks!
Oh My God! So many tasks!

Keep Calm! those were the task open when the app started, we have to wait for the NG0506 warn to appear, and then we can see how many tasks are still open. Let's apply this filter /QTrackNgZoneTasks|NG0506/ and call the tracker after the warning comes up.

That Is Better
That is better, but not ok

Step 2: Fix the open tasks

Here we can use the second function we defined in the qZoneTracker object, printAllSources, to see where those tasks are coming from:

I didn't do it!
I didn't do it!

In the image above, we can see that the first error is probably our since it comes the RxJS interval function. The other two problems come from the keycloak-js library, one from ably.js.

Step 3: Fix the problems

Here is where I let go of your hand and refer you to the official documentation again. In the case found in this example, the solution would be to make all the subscriptions that create intervals wait first for the application to become stable, or run them outside Angular.

And for the libraries, do not start them until the application is stable.

Final thoughts

If I were ChatGPT I would put something like "I hope this helps you to solve the NG0506 error in your Angular application". But I am not, so I will just say that I hope this article helps you to find the open tasks and fix them. If you have any questions or suggestions, feel free to leave a comment below.". But since I am not, I will just invite you to go outside and learn how to skate in Inline Skates, IT IS FUN 😬!