How to find out why Angular SSR hangs - track NgZone tasks 🐾
Tags: angular, ssr, zonejs
Published: 2022-03-15 17:32:14

**Angular Universal SSR (Server Side Rendering) hangs when some asynchronous task in our app is not completed (like a forever-ticking `setInterval()`, recursively-called `setTimeout()` or never-completed HTTP call to API). Since [Zone.js tracks all async tasks inside the Angular app](https://angular.io/guide/zone), we can use Zone.js to identify the exact lines in the code that introduced the forever pending task. All we need is to import the plugin `zone.js/plugins/task-tracking` and after a few seconds look up the deep internal state of the Angular's `NgZone`.**


## 1. Import `zone.js/plugins/task-tracking`
In your `app.module.ts` import the following Zone.js plugin:
```ts
// app.module.ts
import 'zone.js/plugins/task-tracking';
...

```

## 2. Look up the deep internal state of the Angular's `NgZone` after a few seconds
Copy-paste the following `constructor` to your `AppModule`:

```ts
// app.module.ts
...

export class AppModule {
  constructor(ngZone: NgZone) {
    /**
     * CONFIGURE how long to wait (in seconds) 
     * before the pending tasks are dumped to the console.
     */
    const WAIT_SECONDS = 2;

    console.log(
      `⏳ ... Wait ${WAIT_SECONDS} seconds to dump pending tasks ... ⏳`
    );

    // Run the debugging `setTimeout` code outside of
    // the Angular Zone, so it's not considered as 
    // yet another pending Zone Task:
    ngZone.runOutsideAngular(() => {
      setTimeout(() => {
        // Access the NgZone's internals - TaskTrackingZone:
        const TaskTrackingZone = (ngZone as any)._inner
          ._parent._properties.TaskTrackingZone;

        // Print to the console all pending tasks
        // (micro tasks, macro tasks and event listeners):
        console.debug('👀 Pending tasks in NgZone: 👀');
        console.debug({
          microTasks: TaskTrackingZone.getTasksFor('microTask'),
          macroTasks: TaskTrackingZone.getTasksFor('macroTask'),
          eventTasks: TaskTrackingZone.getTasksFor('eventTask'),
        });

        // Advice how to find the origin of Zone tasks:
        console.debug(
          `👀 For every pending Zone Task listed above investigate the stacktrace in the property 'creationLocation' 👆`
        );
      }, 1000 * WAIT_SECONDS);
    });
  }
}
```

## 3. Start your SSR server
Compile and run your SSR app, e.g. run `yarn dev:ssr` (or `npm dev:ssr`)

## 4. Start the rendering
Open a page in the browser (or via another terminal window with command `curl http://localhost:4200`; note: port might be different than 4200 in your case).

## 5. Find out the origin of the pending async task(s)
After a while (e.g. 2 seconds), you should see the list of all pending Zone tasks printed to the console. Each `ZoneTask` object contains a property `creationLocation` which **points to the exact line in the code** which caused this async task. 

Now open the file path listed at the bottom of the stack trace (e.g. Ctrl+click the path on Windows; or Commnad+click on Mac). Then you should see the exact faulty line in the compiled `main.js` that introduced the long time pending task.

## Real Example
For example, here's console output in the app I was debugging:
```
⏳ ... Wait 2 seconds to dump pending tasks ... ⏳

👀 Pending tasks in NgZone: 👀

{
  microTasks: [],
  macroTasks: [
    ZoneTask {
      _zone: [Zone],
      runCount: 0,
      _zoneDelegates: [Array],
      _state: 'scheduled',
      type: 'macroTask',
      source: 'setInterval',
      data: [Object],
      scheduleFn: [Function: scheduleTask],
      cancelFn: [Function: clearTask],
      callback: [Function: timer],
      invoke: [Function (anonymous)],
      creationLocation: Error: Task 'macroTask' from 'setInterval'.
          at TaskTrackingZoneSpec.onScheduleTask (/Users/krzysztof/code/test-ng12-i18next/dist/test-ng12-i18next/server/main.js:177338:36)
          at ZoneDelegate.scheduleTask (/Users/krzysztof/code/test-ng12-i18next/dist/test-ng12-i18next/server/main.js:174750:45)
          at Object.onScheduleTask (/Users/krzysztof/code/test-ng12-i18next/dist/test-ng12-i18next/server/main.js:174648:25)
          at ZoneDelegate.scheduleTask (/Users/krzysztof/code/test-ng12-i18next/dist/test-ng12-i18next/server/main.js:174750:45)
          at Zone.scheduleTask (/Users/krzysztof/code/test-ng12-i18next/dist/test-ng12-i18next/server/main.js:174562:37)
          at Zone.scheduleMacroTask (/Users/krzysztof/code/test-ng12-i18next/dist/test-ng12-i18next/server/main.js:174593:21)
          at scheduleMacroTaskWithCurrentZone (/Users/krzysztof/code/test-ng12-i18next/dist/test-ng12-i18next/server/main.js:175151:25)
          at /Users/krzysztof/code/test-ng12-i18next/dist/test-ng12-i18next/server/main.js:177066:22
          at proto.<computed> (/Users/krzysztof/code/test-ng12-i18next/dist/test-ng12-i18next/server/main.js:175438:18)
👉👉👉👉👉at Backend.init (/Users/krzysztof/code/test-ng12-i18next/dist/test-ng12-i18next/server/main.js:178558:9),
      ref: [Function: bound ],
      unref: [Function: bound ]
    }
  ],
  eventTasks: []
}
👀 For every pending Zone Task listed above investigate the stacktrace in the property 'creationLocation' 👆
```

The faulty line in my case was `setInterval()` which was never disposed:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5s4bnvseiywyf3retmkq.png)
... and by the way it was coming from a 3rd party dependency package - `i18next-http-backend` (see [source code](https://github.com/i18next/i18next-http-backend/blob/00b7e8f67abf8372af17529b51190a7e8b17e3d8/lib/index.js#L41)). Then I [fixed](https://github.com/SAP/spartacus/pull/15297/files#diff-b9c2f7d32db998292ce5b32da446437ad64755d84817c3261fdbc89f6438856aR48) the hanging render just by setting the option `backend.reloadInterval` to `false` in the options of `i18next`.


## Caveats
At the time of writing (2022-03-15, zone.js v0.11.5) there exists is a [bug](https://github.com/angular/angular/issues/45350) in `TaskTrackingZone`. If the `setInterval()` has a shorter periodic timer value (e.g. 1000ms) than our debugging script's delay time (e.g. 2000ms), then this `setInterval` task **won't be logged in the list of pending Zone's macrotasks!** When the callback of `setInverval(callback, ms)` was invoked for the first time, then the task was removed from the array of tracked tasks in `TaskTrackingZone`. See [source code of TaskTrackingZone](https://github.com/angular/angular/blob/d1ea1f4c7f3358b730b0d94e65b00bc28cae279c/packages/zone.js/lib/zone-spec/task-tracking.ts#L66).



To fix this bug locally, you would need to change [this line](https://github.com/angular/angular/blob/d1ea1f4c7f3358b730b0d94e65b00bc28cae279c/packages/zone.js/lib/zone-spec/task-tracking.ts#L61) in your node_modules `node_modules/zone.js/fesm2015/task-tracking.js`:
```diff
- if (task.type === 'eventTask')
+ if (task.type === 'eventTask' || (task.data && task.data.isPeriodic))
```

## Bonus: use handy lib `ngx-zone-task-tracking` instead of above code snippets 
To make our lives easier, I published the npm package [ngx-zone-task-tracking](https://github.com/Platonn/ngx-zone-task-tracking) which prints to the console with a delay all the pending NgZone macrotasks and by the way fixes locally [the bug mentioned before in `TaskTrackingZone`](https://github.com/angular/angular/issues/45350). All you need is to `npm install ngx-zone-task-tracking` and import `ZoneTaskTrackingModule.printWithDelay(2000)` in your app module:
```ts
import { ZoneTaskTrackingModule } from 'ngx-zone-task-tracking';
/* ... */

@NgModule({
  imports: [
    ZoneTaskTrackingModule.printWithDelay(/* e.g. */ 2000)
  ]
})
export class AppModule {}
``` 
Here's the [live demo](https://stackblitz.com/edit/ngx-zone-task-tracking?file=src%2Fapp%2Fapp.module.ts) of `ngx-zone-task-tracking`.

## Conclusion
Our Angular applications run plenty of small async operations. When the Angular Universal SSR hangs, it might be not obvious which async task(s) is forever pending. Fortunately, with the help of the plugin `zone.js/plugins/task-tracking` and checking the internal state of Angular's `NgZone` we can locate faulty lines in the source code (of our own or of 3rd party package). Then we know where to fix the hanging SSR.

### Update 2022-04-07
I've [fixed the bug mentioned above directly in the Angular repo](https://github.com/angular/angular/commit/f19b36f462803b3b3b9410391c039649541b10bc)! 🎉 (for more, see the article ["How I became the Angular contributor 🙃"](https://dev.to/krisplatis/how-i-became-the-angular-contributor-2066)). Now, I'm waiting for the new patch version of `zone.js` to be published to npm.

### Update 2023-07-03
I've learned only recently in details, how Angular tracks pending HTTP requests under the hood and how it changed since version 16.

#### Before Angular 16
Before Angular 16, HTTP requests were wrapped by Angular as artificially-created macrotasks (even though HTTP requests are not JavaScript macrotasks by nature). Thanks to that, `Zone.js` could keep track of pending HTTP calls and `NgZone.onStable` could emit when all async timers and HTTP requests completed.

The artifical macrotask created by Angular had a fancy name [`'ZoneMacroTaskWrapper.subscribe'`](https://github.com/angular/angular/blob/15.2.x/packages/platform-server/src/http.ts#L85-L87). Unfortunately, to my knowledge, when printing to the console such a macrotask, you cannot find it's origin by stracktrace, nor find any details of the original request.

#### Since Angular 16
Since Angular 16, Angular no longer uses the trick of wrapping HTTP requests as `Zone.js` macrotasks. So you will no longer see any `'ZoneMacroTaskWrapper.subscribe'` when printing to console all pending macro tasks. To track pending HTTP calls you can write a custom [`HttpInterceptor`](https://angular.io/api/common/http/HttpInterceptor). To learn more, see the article: [Angular SSR v16: saying goodbye to a sneaky trick - macrotask wrapping for HTTP calls 👋](https://dev.to/krisplatis/angular-ssr-v16-saying-goodbye-to-a-sneaky-trick-macrotask-wrapping-for-http-calls-487b).


## References

- source code of `@angular/core`: `NgZone` sets `TaskTrackingZone` as its `_inner` zone - https://github.com/angular/angular/blob/215db7fbe6c91c43383a784b8d74c8063ce5c340/packages/core/src/zone/ng_zone.ts#L138-L140
- source code of `zone.js/plugins/task-tracking`: `TaskTrackingZone` captures the stacktrace of each task - https://github.com/angular/angular/blob/d1ea1f4c7f3358b730b0d94e65b00bc28cae279c/packages/zone.js/lib/zone-spec/task-tracking.ts#L40