Top 5 Mistakes Developers Make During Angular Interviews (And How to Avoid Them)

·12 min read
angularinterview-questionsinterview-tipsfrontendcareer

I've conducted hundreds of Angular interviews over 12 years in fintech. The same mistakes keep appearing. Senior developers with 5+ years of experience trip on the same questions that junior developers struggle with. Not because Angular is hard to use - because Angular is easy to use without understanding how it works.

These five mistakes cost developers job offers. I've seen candidates fail on exactly these points at companies like BNY Mellon, UBS, and major tech firms. Here's how to avoid them.

Mistake #1: Misunderstanding Change Detection and OnPush

This is the most common interview failure point. A candidate explains they've used Angular for years, then can't explain why their OnPush component stopped updating.

The Mistake

@Component({
  selector: 'app-user-list',
  template: `
    <div *ngFor="let user of users">{{ user.name }}</div>
    <button (click)="addUser()">Add User</button>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserListComponent {
  @Input() users: User[] = [];
 
  addUser() {
    // This WILL NOT update the view!
    this.users.push({ id: 99, name: 'New User' });
  }
}

When asked "Why isn't the view updating?", weak answers include:

  • "Maybe there's a bug?"
  • "I'd need to see the full code"
  • "Change detection must be broken"

The Strong Answer

"OnPush change detection uses reference equality (===) to check if inputs changed. When I call push(), I'm mutating the existing array - the reference stays the same. Angular sees the same object reference and skips the component.

The fix is to create a new array reference:

this.users = [...this.users, { id: 99, name: 'New User' }];

Or I could inject ChangeDetectorRef and call markForCheck() or detectChanges() after the mutation."

What Interviewers Want to Hear

  1. You understand immutability - OnPush requires new references, not mutations
  2. You know the workarounds - Spread operator, Object.assign(), or manual ChangeDetectorRef
  3. You can explain when to use OnPush - Performance optimization for components that receive data via @Input

The Deeper Follow-Up

If they ask "What triggers change detection with OnPush?", be ready:

"Four things trigger OnPush checks:

  1. An @Input reference changes (not content, the reference itself)
  2. An event originates from the component or its children
  3. You explicitly call detectChanges() or markForCheck()
  4. An async pipe receives a new value

Everything else is ignored, which is why OnPush dramatically reduces checking overhead in large applications."

Mistake #2: Memory Leaks from Unsubscribed Observables

Every Angular interview covers RxJS. The question that catches people: "How do you prevent memory leaks with Observables?"

The Mistake

@Component({
  selector: 'app-dashboard',
  template: `<div>{{ data }}</div>`
})
export class DashboardComponent implements OnInit {
  data: string = '';
 
  constructor(private dataService: DataService) {}
 
  ngOnInit() {
    // Memory leak waiting to happen
    this.dataService.getData().subscribe(result => {
      this.data = result;
    });
  }
}

Weak answers:

  • "I unsubscribe in ngOnDestroy" (but then can't show the code)
  • "The component gets destroyed so it's fine" (wrong - subscription survives)
  • "I use takeUntil but I'm not sure how" (concerning uncertainty)

The Strong Answer

"There are three reliable patterns to prevent memory leaks:

1. The async pipe (preferred when possible):

@Component({
  template: `<div>{{ data$ | async }}</div>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class DashboardComponent {
  data$ = this.dataService.getData();
}

The async pipe subscribes when the component renders and unsubscribes when it's destroyed. It also calls markForCheck() automatically, making it perfect with OnPush.

2. takeUntil with a destroy subject:

private destroy$ = new Subject<void>();
 
ngOnInit() {
  this.dataService.getData()
    .pipe(takeUntil(this.destroy$))
    .subscribe(result => this.data = result);
}
 
ngOnDestroy() {
  this.destroy$.next();
  this.destroy$.complete();
}

3. Collecting subscriptions (older pattern):

private subscriptions = new Subscription();
 
ngOnInit() {
  this.subscriptions.add(
    this.dataService.getData().subscribe(...)
  );
}
 
ngOnDestroy() {
  this.subscriptions.unsubscribe();
}

I prefer the async pipe because it's declarative, works with OnPush, and eliminates the possibility of forgetting to unsubscribe."

When You DON'T Need to Unsubscribe

Knowing what doesn't need cleanup shows deeper understanding:

"Some Observables complete naturally and don't need manual unsubscription:

  • HTTP requests from HttpClient (they complete after one response)
  • Router events (handled by Angular internally)
  • Observables created with of() or from() (they complete immediately)

But infinite streams like interval(), fromEvent(), and store selectors always need cleanup."

Mistake #3: Confusion About Dependency Injection Scopes

"How do you ensure a service is a singleton?" This question reveals whether you understand Angular's injector hierarchy.

The Mistake

// Candidate says: "I add it to providers"
@NgModule({
  providers: [UserService]  // But which injector?
})
export class UsersModule { }

Then asked "What if UsersModule is lazy-loaded?" - silence.

The Strong Answer

"Angular has a hierarchical injector system. Where you provide a service determines its scope:

1. Root singleton (recommended):

@Injectable({ providedIn: 'root' })
export class UserService { }

This creates exactly one instance for the entire application, regardless of where it's injected. It also enables tree-shaking - if the service isn't used, it's removed from the bundle.

2. Module-level provider:

@NgModule({
  providers: [UserService]
})
export class UsersModule { }

If UsersModule is eagerly loaded, UserService goes to the root injector (singleton). If UsersModule is lazy-loaded, a new child injector is created. The service becomes a singleton within that lazy-loaded module only.

3. Component-level provider:

@Component({
  providers: [UserService]
})
export class UserComponent { }

Every instance of UserComponent gets its own UserService instance. The service is destroyed with the component."

The Follow-Up Trap

"What if you need to inject a value that isn't a class?"

"I use InjectionToken for non-class values:

export const API_URL = new InjectionToken<string>('apiUrl');
 
// In module
providers: [
  { provide: API_URL, useValue: 'https://api.example.com' }
]
 
// In service
constructor(@Inject(API_URL) private apiUrl: string) { }

InjectionToken is type-safe and prevents the magic string problems of the old string-based injection."

Mistake #4: Misusing Lifecycle Hooks

"What's the difference between the constructor and ngOnInit?" This fundamental question trips up surprisingly many candidates.

The Mistake

@Component({
  selector: 'app-user',
  template: `<div>{{ userData?.name }}</div>`
})
export class UserComponent {
  @Input() userId!: number;
  userData: User;
 
  constructor(private userService: UserService) {
    // MISTAKE: Input isn't set yet!
    this.userService.getUser(this.userId).subscribe(
      user => this.userData = user
    );
  }
}

Weak answer: "ngOnInit is just where Angular wants you to put initialization code."

The Strong Answer

"The constructor and ngOnInit serve different purposes:

Constructor:

  • Called by JavaScript, not Angular
  • @Input properties are NOT set yet
  • Only use for dependency injection
  • Keep it simple - no initialization logic

ngOnInit:

  • Called by Angular after setting @Input properties
  • First change detection has run
  • This is where initialization logic belongs
export class UserComponent implements OnInit {
  @Input() userId!: number;
 
  constructor(private userService: UserService) {
    // userId is undefined here
  }
 
  ngOnInit() {
    // userId is now available
    this.loadUser();
  }
}

The lifecycle order matters:

  1. Constructor
  2. ngOnChanges (first call, inputs are set)
  3. ngOnInit
  4. ngDoCheck
  5. ngAfterContentInit
  6. ngAfterViewInit
  7. ... (repeated on changes)
  8. ngOnDestroy"

The Senior-Level Extension

"Understanding the full lifecycle becomes critical for complex components:

  • ngOnChanges: Called before ngOnInit and whenever @Input values change. Receives a SimpleChanges object with previous and current values.

  • ngAfterViewInit: Safe to access @ViewChild references here, not before. A common mistake is trying to access view children in ngOnInit.

  • ngAfterContentInit: Safe to access @ContentChild references (projected content).

@Component({...})
export class ChartComponent implements AfterViewInit {
  @ViewChild('canvas') canvas!: ElementRef<HTMLCanvasElement>;
 
  ngOnInit() {
    // this.canvas is undefined!
  }
 
  ngAfterViewInit() {
    // this.canvas is now available
    this.initChart();
  }
}
```"

Mistake #5: Ignoring Performance Best Practices

"How would you optimize an Angular application?" This open-ended question separates those who've dealt with real performance issues from those who haven't.

The Mistake

A candidate mentions "use OnPush" but can't explain specific optimizations or why they matter.

The Strong Answer

"I focus on these key areas:

1. Never call methods in templates:

// BAD - formatDate() runs on every change detection
template: `<div>{{ formatDate(user.createdAt) }}</div>`
 
// GOOD - Pure pipe only recalculates when input changes
template: `<div>{{ user.createdAt | date:'medium' }}</div>`

Methods execute every change detection cycle. In an app checking 500 components, that's 500 function calls per user interaction.

2. Always use trackBy with ngFor:

<div *ngFor="let item of items; trackBy: trackById">
  {{ item.name }}
</div>
 
trackById(index: number, item: Item): number {
  return item.id;
}

Without trackBy, Angular destroys and recreates all DOM elements when the array changes. With trackBy, it only updates elements that actually changed.

3. Lazy load feature modules:

const routes: Routes = [{
  path: 'admin',
  loadChildren: () => import('./admin/admin.module')
    .then(m => m.AdminModule)
}];

Users only download the admin module when they navigate to it.

4. Use OnPush everywhere possible: Start with OnPush as the default. Only use Default strategy when you have a specific reason.

5. Run expensive code outside Angular's zone:

this.ngZone.runOutsideAngular(() => {
  // Animations, scroll handlers, mousemove
  // No change detection triggered
});
```"

The Real-World Follow-Up

"Walk me through diagnosing a slow Angular application."

"I follow this process:

  1. Profile with Chrome DevTools - Look at the Performance tab for long frames

  2. Check change detection frequency - Add console.log to templates. If it fires constantly, something's triggering unnecessary checks

  3. Use Angular DevTools - The profiler shows exactly which components are checking and how long each takes

  4. Look for common culprits:

    • Methods or getters in templates
    • Missing trackBy in ngFor loops
    • Components not using OnPush
    • Subscriptions firing too frequently
  5. Bundle analysis - Run ng build --stats-json and use webpack-bundle-analyzer to find oversized dependencies

The fix is usually one of: add OnPush, add trackBy, convert method to pipe, or lazy load a module."

Bonus: The Question That Reveals Everything

After discussing these specifics, interviewers often ask: "If you were building a new Angular application today, what patterns would you establish from day one?"

This is your chance to show architectural thinking:

"I'd establish these patterns:

  1. OnPush by default - Create a schematic that generates components with OnPush

  2. Async pipe everywhere - Ban manual subscriptions in templates. Use the async pipe and handle loading/error states declaratively

  3. Smart/dumb component pattern - Container components handle data fetching, presentational components only receive @Input and emit @Output

  4. Centralized error handling - HTTP interceptor for API errors, global error handler for unexpected exceptions

  5. Strict TypeScript - Enable strictNullChecks, noImplicitAny, strictPropertyInitialization. Catch bugs at compile time

  6. Lazy loading from the start - Define feature module boundaries early. It's harder to add lazy loading later

These patterns prevent the performance and maintainability issues that make Angular applications hard to work with at scale."

Quick Reference: What to Say (and Not Say)

TopicWeak AnswerStrong Answer
OnPush not updating"Change detection is broken""Mutation doesn't change reference - use spread operator"
Memory leaks"I unsubscribe somewhere""Async pipe is preferred, or takeUntil pattern"
Service singleton"Add to providers""providedIn: 'root' for tree-shakeable singleton"
Constructor vs ngOnInit"ngOnInit is Angular's way""Constructor: DI only. ngOnInit: inputs are set"
Performance"Use OnPush""TrackBy, pure pipes, lazy loading, avoid template methods"

Practice Before Your Interview

Go through each mistake and make sure you can:

  1. Recognize the problem in code
  2. Explain why it's wrong (the underlying mechanism)
  3. Show the fix with correct code
  4. Discuss trade-offs when there are multiple solutions

The developers who get offers aren't necessarily the ones who've memorized the most. They're the ones who can diagnose problems and explain their reasoning clearly.


Related Articles

If you found this helpful, check out these related guides:

Ready for More Angular Interview Questions?

These five mistakes are just the beginning. Our complete Angular interview prep covers 50+ questions on:

  • Components, Directives, and Pipes
  • Dependency Injection patterns
  • RxJS and reactive programming
  • Routing and guards
  • Forms (Template-driven and Reactive)
  • Testing strategies
  • Angular 19+ features

Get Full Access to All Angular Questions

Or try our free Angular preview to see more questions like this.


Written by the EasyInterview team, based on real interview experience from 12+ years in tech and hundreds of technical interviews conducted at companies like BNY Mellon, UBS, and leading fintech firms.

Ready to ace your interview?

Get 550+ interview questions with detailed answers in our comprehensive PDF guides.

View PDF Guides