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:
// 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);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:
// .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}"
}
]
}- Start your dev server:
ng serve - Press F5 in VS Code to launch Chrome with debugging
- Set breakpoints directly in TypeScript source files
- 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:
// 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
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
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
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:
// 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
// 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
}
}
}"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:
- Open DevTools → Angular → Profiler tab
- Click "Start recording"
- Perform the slow interaction in your app
- Click "Stop recording"
- Inspect the flame chart — look for components with high CD duration
- Click a component to see which bindings caused re-renders
Debugging Router Issues
// 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
],
};// 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