AngularJSDebugging

Debugging Angular Applications

Debugging Angular applications requires knowing which tools to reach for. Angular provides excellent browser devtools integration, a dedicated Chrome extension, and multiple runtime inspection APIs.

This guide covers the full debugging toolkit: from browser console tricks to Angular DevTools, common error messages, and debugging Signals and RxJS.

Angular DevTools (Chrome Extension)

Angular DevTools is the official Chrome/Edge extension for inspecting Angular apps. Install it from the Chrome Web Store, then open DevTools and look for the Angular tab.

Features:

  • Component tree — browse your full component hierarchy

  • Component properties — inspect and edit inputs, outputs, and state in real time

  • Injector tree — see the DI hierarchy and resolve providers

  • Profiler — record and analyze change detection cycles to find performance bottlenecks

  • Router overlay — visualize route state transitions

Angular DevTools requires your app to be running in development mode. It works automatically with ng serve but not with a production build.

Console Debugging with ng

Angular exposes a global ng object in development mode for console debugging:

JS
// In your browser console:

// Get component instance for a DOM element
const el = document.querySelector('app-user-profile');
const component = ng.getComponent(el);
console.log(component);          // Inspect all properties
component.username = 'Debug';    // Modify state
ng.applyChanges(el);             // Trigger change detection manually

// Get injector and resolve services
const injector = ng.getInjector(el);
const userService = injector.get(UserService);
console.log(userService.currentUser$);

// Get the host element from a component instance
const host = ng.getHostElement(component);

// Get directive on an element
const directive = ng.getDirectiveMetadata(el);

// List all components in the app
ng.getOwningComponent(el);
Note
The ng object is only available in development builds. Production builds strip these APIs to reduce bundle size and prevent manipulation.
Using Breakpoints in VS Code

Set up VS Code debugging for Angular:

JSON
// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "chrome",
      "request": "launch",
      "name": "Launch Chrome against localhost",
      "url": "http://localhost:4200",
      "webRoot": "${workspaceFolder}"
    },
    {
      "type": "chrome",
      "request": "attach",
      "name": "Attach to Chrome",
      "port": 9222,
      "webRoot": "${workspaceFolder}"
    }
  ]
}
  1. Start your dev server: ng serve
  2. Press F5 in VS Code to launch Chrome with debugging
  3. Set breakpoints directly in TypeScript source files
  4. Angular CLI generates source maps automatically in development mode
Common Error Messages and Fixes

Error

Cause

Fix

NullInjectorError: No provider for X

Service not provided in the current injector tree

Add to providers array or use providedIn: root

ExpressionChangedAfterItHasBeenCheckedError

Value changed after Angular finished change detection

Move logic to ngAfterViewInit or use setTimeout/queueMicrotask

Can't bind to 'ngModel' since it isn't a known property

FormsModule not imported

Import FormsModule or ReactiveFormsModule

Template parse error: 'app-x' is not a known element

Component not imported in standalone or module

Add component to imports array

TypeError: Cannot read properties of undefined

Accessing property before data loads

Use optional chaining (?.) or @if guard in template

NG0100: ExpressionChangedAfterItHasBeenChecked

Two-way binding causes a second CD pass

Use ChangeDetectorRef.detectChanges() or signals

Debugging ExpressionChangedAfterItHasBeenCheckedError

This is one of the most common Angular errors. It means a binding value changed after Angular's change detection pass finished. The typical fix:

TS
// Problem: value changes in ngAfterViewInit
export class ParentComponent implements AfterViewInit {
  @ViewChild(ChildComponent) child!: ChildComponent;
  title = '';

  ngAfterViewInit() {
    this.title = this.child.title; // ❌ Changes after CD completed
  }
}

// Fix 1: Use setTimeout to defer the change
ngAfterViewInit() {
  setTimeout(() => {
    this.title = this.child.title; // ✅ Runs in next tick
  });
}

// Fix 2: Use ChangeDetectorRef
constructor(private cdr: ChangeDetectorRef) {}

ngAfterViewInit() {
  this.title = this.child.title;
  this.cdr.detectChanges(); // ✅ Runs another CD pass
}

// Fix 3: Use Signals (Angular 16+)
title = signal('');

ngAfterViewInit() {
  this.title.set(this.child.title); // ✅ Signals handle async updates gracefully
}
Debugging Change Detection

TS
import { Component, ChangeDetectionStrategy, ChangeDetectorRef, inject } from '@angular/core';

@Component({
  selector: 'app-debug-cd',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `<p>{{ value }}</p><button (click)="update()">Update</button>`,
})
export class DebugCdComponent {
  value = 'initial';
  private cdr = inject(ChangeDetectorRef);

  // Add this to any lifecycle hook to trace when CD runs
  ngDoCheck() {
    console.log('Change detection ran');
  }

  update() {
    this.value = 'updated';
    // With OnPush, you must manually trigger CD after external changes:
    this.cdr.markForCheck(); // Mark component for next CD pass
    // OR
    this.cdr.detectChanges(); // Run CD immediately for this subtree
  }
}
Debugging Signals

TS
import { signal, computed, effect } from '@angular/core';

// Use effect() to trace signal changes during development
const count = signal(0);
const doubled = computed(() => count() * 2);

// Debugging effect — prints whenever count changes
const debugEffect = effect(() => {
  console.log(`[DEBUG] count changed to: ${count()}`);
  console.log(`[DEBUG] doubled is: ${doubled()}`);
});

// In a component
@Component({ standalone: true, template: '' })
export class DebugComponent {
  count = signal(0);

  constructor() {
    effect(() => {
      // This runs reactively whenever count changes
      console.log('Count signal value:', this.count());
    });
  }
}
Debugging RxJS Observables

TS
import { Observable } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';

// Use tap() for non-intrusive debugging
this.userService.getUsers().pipe(
  tap((users) => console.log('Fetched users:', users)),
  tap({
    next: (val) => console.log('[next]', val),
    error: (err) => console.error('[error]', err),
    complete: () => console.log('[complete]'),
  }),
).subscribe();

// Debug operator — custom tap wrapper
function debug<T>(tag: string) {
  return tap<T>({
    next: (val) => console.log(`[${tag}] next:`, val),
    error: (err) => console.error(`[${tag}] error:`, err),
    complete: () => console.log(`[${tag}] complete`),
  });
}

// Usage:
this.data$.pipe(
  debug('UserData'),
  map((data) => data.users),
  debug('AfterMap'),
).subscribe();
Network Debugging

Debug HTTP requests using Angular's interceptors or the browser Network tab:

TS
// logging.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { tap } from 'rxjs/operators';

export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
  console.log(`[HTTP] ${req.method} ${req.url}`);
  const start = Date.now();

  return next(req).pipe(
    tap({
      next: (event) => {
        if (event.type === 4) { // HttpEventType.Response
          const duration = Date.now() - start;
          console.log(`[HTTP] ${req.method} ${req.url} - ${duration}ms`);
        }
      },
      error: (err) => {
        console.error(`[HTTP] ${req.method} ${req.url} FAILED:`, err);
      },
    })
  );
};

// Register in app.config.ts:
provideHttpClient(withInterceptors([loggingInterceptor]))
Source Maps Configuration

JSON
// angular.json — ensure source maps are enabled in dev
{
  "configurations": {
    "development": {
      "buildOptimizer": false,
      "optimization": false,
      "sourceMap": true,
      "extractLicenses": false,
      "namedChunks": true
    },
    "production": {
      "sourceMap": false  // Disable in production for security
    }
  }
}
Tip
Enable "sourceMap": true in your production build temporarily if you need to debug a production issue. Remove it immediately after — source maps expose your source code.
Profiling with Angular DevTools

Use the Angular DevTools Profiler to find slow change detection:

  1. Open DevTools → Angular → Profiler tab
  2. Click "Start recording"
  3. Perform the slow interaction in your app
  4. Click "Stop recording"
  5. Inspect the flame chart — look for components with high CD duration
  6. Click a component to see which bindings caused re-renders
Debugging Router Issues

TS
// Enable router tracing (logs every router event)
import { provideRouter, withDebugTracing } from '@angular/router';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withDebugTracing()), // Add this in development only
  ],
};

TS
// Manually trace router events
import { Router, NavigationStart, NavigationEnd, NavigationError } from '@angular/router';
import { inject } from '@angular/core';

export class AppComponent {
  private router = inject(Router);

  constructor() {
    this.router.events.subscribe((event) => {
      if (event instanceof NavigationStart) {
        console.log('Navigation started:', event.url);
      }
      if (event instanceof NavigationEnd) {
        console.log('Navigation ended:', event.url);
      }
      if (event instanceof NavigationError) {
        console.error('Navigation error:', event.error);
      }
    });
  }
}
Quick Debug Checklist
  • Install Angular DevTools Chrome extension for component inspection

  • Check browser console for error messages with stack traces

  • Use ng.getComponent(el) in console to inspect component instances

  • Add tap() operators to debug Observable pipelines

  • Use effect() to trace Signal value changes

  • Enable withDebugTracing() on provideRouter for routing issues

  • Check NullInjectorError — missing provider in the correct injector scope

  • For ExpressionChangedAfterChecked — defer with setTimeout or use Signals