Angular Interview Questions and Answers: 60 Questions from Easy to Hard
This guide is for Indian job seekers preparing for Angular developer interviews: freshers going for their first front-end role and experienced developers aiming for senior positions at product companies, IT services firms and startups. It has 60 questions in three levels, written for Angular 17 and 18. Easy questions cover components, templates, binding, standalone components, the new @if and @for control flow, services and basic signals. Medium questions cover computed and effect, signal inputs, RxJS operators, unsubscribing, OnPush, guards, lazy loading, interceptors, reactive forms and testing. Hard questions go into change detection internals, zoneless apps, DI hierarchies, SSR, security and performance. Read each answer aloud in your own words, type the code into a fresh ng new project, then take the mock test.
- 60 questions, 3 levels
- Updated
- CoreHRx Jobs
- 793 open Angular jobs
What Angular interviews cover
- Fundamentals: components, templates, the four kinds of data binding, pipes, directives, lifecycle hooks and parent-child communication with inputs and outputs.
- Modern Angular (17 and 18): standalone components, bootstrapApplication, the built-in @if, @for and @switch control flow, @defer blocks and signals with computed and effect.
- Application structure: services and dependency injection, routing with guards and lazy loading, HttpClient with interceptors, and reactive versus template-driven forms.
- RxJS: Observables versus Promises, Subjects, switchMap versus mergeMap, error handling, sharing results and unsubscribing without memory leaks.
- Senior topics: change detection and OnPush, Zone.js and zoneless apps, bundle size and runtime performance, SSR with hydration, security and testing strategy.
How to prepare (freshers and experienced)
- Freshers: get TypeScript basics right first (types, interfaces, classes, generics, async and await), because every Angular answer builds on them.
- Create a project with ng new and build one small app end to end, such as a job search page with filters, a detail route, a login guard and a reactive form.
- Learn the Angular 17 and 18 way of writing code (standalone, @if and @for, signals, inject, functional guards and interceptors) but also be able to read older NgModule code, since many company projects still use it.
- Experienced developers: prepare two or three stories from real projects, such as a slow page you made faster, a memory leak you fixed or a large module you migrated, with numbers where you have them.
- Practise RxJS on paper: draw what switchMap, mergeMap, concatMap and exhaustMap do with three quick clicks, because interviewers love that question.
Common mistakes and interview-day tips
- Do not mix up versions: say clearly which features are Angular 17 or 18 (control flow, @defer, signal inputs, zoneless) and which are older.
- Never claim that a route guard secures data; it only controls navigation, and the server must still check every request.
- When asked about performance, start with how you would measure (Angular DevTools profiler, Chrome performance panel, bundle analysis) before listing fixes.
- In a live coding round, keep components small, type your data with interfaces, unsubscribe properly and explain your choices while you type.
- If you do not know an API name exactly, describe the behaviour you need and how you would find it in the official docs; that is better than guessing.
Angular interview questions: Easy questions (freshers)
20 questions1. What is Angular, and how is it different from AngularJS?
Angular is a TypeScript-based framework from Google for building single-page applications. It gives you components, templates, dependency injection, a router, forms, an HTTP client and a CLI in one package, so teams do not have to pick each piece. AngularJS was the older 1.x framework written in plain JavaScript and built around controllers and $scope; its long-term support ended in December 2021. Angular was a full rewrite, so the two share only the name. A common slip is calling Angular 17 "AngularJS 17".
2. What is a component, and what goes into the @Component decorator?
A component is a TypeScript class that controls a piece of the screen. The @Component decorator tells Angular how to use it: selector is the custom tag name, template or templateUrl holds the HTML, styles or styleUrl holds the CSS, and in Angular 17 and 18 standalone: true plus an imports array lists what the template uses. Class fields become the data the template displays. Keep components small and focused: one for a job card, another for the list that repeats it.
import { Component } from '@angular/core';
@Component({
selector: 'app-job-card',
standalone: true,
template: `
<h3>{{ title }}</h3>
<p>{{ city }}</p>
`,
styles: [`h3 { color: #7048e8; }`],
})
export class JobCardComponent {
title = 'Frontend Developer';
city = 'Pune';
}3. What are the four kinds of data binding in Angular?
Interpolation, written with double curly braces, shows a value as text. Property binding, with square brackets, sets a DOM or component property from the class. Event binding, with round brackets, calls a method when something happens, and $event carries the event data. Two-way binding, the banana in a box [( )], combines a property binding and an event binding, as ngModel does with FormsModule. Data flows from class to view in the first two, view to class in event binding, and both ways in two-way binding.
<h2>{{ job.title }}</h2>
<img [src]="job.logoUrl" [alt]="job.company" />
<button (click)="apply(job.id)">Apply</button>
<input [(ngModel)]="searchText" placeholder="Search jobs" />4. What is attribute binding, and how is it different from property binding?
Property binding sets a property on the DOM element object, while attribute binding sets the HTML attribute itself using the attr. prefix. You need attribute binding when there is no matching property, as with aria-* attributes, data-* attributes and many SVG attributes. If the bound value is null or undefined, Angular removes the attribute. The same bracket syntax also gives class binding, which adds or removes one CSS class, and style binding, which can include a unit such as px.
<button [attr.aria-label]="'Save ' + job.title">Save</button>
<li [attr.data-job-id]="job.id">{{ job.title }}</li>
<div [class.active]="isSelected">...</div>
<div [style.width.px]="barWidth"></div>5. What is a standalone component, and why do new Angular 17 projects use them?
A standalone component lists its own dependencies in its imports array instead of being declared in an NgModule. From Angular 17, ng new creates a standalone app by default: main.ts calls bootstrapApplication with the root component and a list of providers such as provideRouter and provideHttpClient. This removes a layer of boilerplate, makes each component easier to read and test, and makes lazy loading a single component simple. In Angular 17 and 18 you still write standalone: true; Angular 19 made it the default.
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [provideRouter(routes), provideHttpClient()],
});6. How does the @if block work in Angular 17?
@if is part of the built-in control flow added in Angular 17 (developer preview there, stable in 18). It shows its content only when the condition is truthy, and supports @else if and @else blocks. With as you can store the result of the condition in a local name, which is handy for a signal or async value. Unlike *ngIf, it needs no import in a standalone component because the compiler understands it directly. Older code can be converted with the ng generate @angular/core:control-flow schematic.
@if (user(); as u) {
<p>Welcome back, {{ u.name }}</p>
} @else if (loading()) {
<p>Loading your profile...</p>
} @else {
<a routerLink="/login">Sign in</a>
}7. How does @for work, and why is track required?
@for repeats a block for each item of a collection. The track expression is mandatory: it tells Angular how to identify each item so that, when the list changes, it can move or reuse existing DOM nodes instead of rebuilding all of them. Track by a stable id from your data; tracking by object identity means a fresh API response rebuilds every row. Variables such as $index, $first, $last and $count are available, and an @empty block shows when the list has no items.
<ul>
@for (job of jobs(); track job.id; let i = $index) {
<li>{{ i + 1 }}. {{ job.title }} - {{ job.city }}</li>
} @empty {
<li>No openings in this city yet.</li>
}
</ul>8. What is the difference between ng-container and ng-template?
ng-container is a grouping element that renders nothing itself, so you can wrap content or apply a directive without adding an extra div that breaks your CSS layout. ng-template defines a piece of template that is not rendered at all until something asks for it: a structural directive, ngTemplateOutlet, or ViewContainerRef. A typical use is a reusable empty-state or loading template that you pass context into. Freshers often expect an ng-template on its own to appear on the page; it never does.
<ng-container
*ngTemplateOutlet="emptyState; context: { $implicit: city }">
</ng-container>
<ng-template #emptyState let-city>
<p>No openings in {{ city }} right now.</p>
</ng-template>9. What is a service, and what does providedIn: root mean?
A service is a class that holds logic or data you want to share across components, such as API calls, the logged-in user or a shopping cart. Marking it with @Injectable and providedIn: root registers it with the root injector, so the whole app gets one shared instance, created the first time something injects it. It is also tree-shakable: if nothing injects it, the build drops it. Keeping HTTP calls in services instead of components makes components thinner and much easier to test.
@Injectable({ providedIn: 'root' })
export class JobService {
private http = inject(HttpClient);
getJobs(city: string) {
return this.http.get<Job[]>('/api/jobs', { params: { city } });
}
}10. What is dependency injection, and when would you use inject() instead of the constructor?
Dependency injection means a class asks for what it needs and Angular supplies it, instead of the class creating it with new. That makes code loosely coupled and easy to test with fakes. You can ask through constructor parameters or with the inject() function. inject() also works in functional guards, interceptors and resolvers, and it keeps subclasses simple. It only works in an injection context: a constructor, a field initializer or a provider factory. Calling it in ngOnInit or a click handler throws error NG0203.
11. Explain the main component lifecycle hooks, and why data is loaded in ngOnInit and not the constructor.
The constructor runs when Angular creates the class, before any inputs are set, so it should only receive dependencies. ngOnChanges runs before ngOnInit when the component has bound inputs, and again whenever they change. ngOnInit runs once after the first inputs arrive, which makes it the right place to start loading data that depends on them. ngAfterViewInit runs when the template and its child components exist, and ngOnDestroy runs just before removal for cleanup. Reading an input in the constructor gives its default value, not what the parent bound.
12. How do a parent and child component talk to each other with @Input and @Output?
The parent passes data down with property binding to an @Input on the child, and the child sends events up through an @Output that is an EventEmitter. The child should not change the parent data directly; it emits an event and the parent decides what to do. Since Angular 16 an input can be marked required, so a missing binding is a build error. Angular 17 also added the signal-based input() and output() functions for the same job. For components that are far apart, use a shared service instead.
@Component({
selector: 'app-job-item',
standalone: true,
template: `
<h4>{{ job.title }}</h4>
<button (click)="apply.emit(job.id)">Apply</button>
`,
})
export class JobItemComponent {
@Input({ required: true }) job!: Job;
@Output() apply = new EventEmitter<number>();
}
// parent: <app-job-item [job]="j" (apply)="onApply($event)" />13. What are pipes, and how do you write a custom pipe?
A pipe transforms a value for display in the template without changing the data itself, for example date, currency, uppercase, percent, json and async. You chain them with the pipe character and pass arguments after a colon. A custom pipe is a class with the @Pipe decorator and a transform method. Pipes are pure by default, so Angular re-runs them only when the input value or object reference changes, which makes them cheaper than calling a method in the template. Here 250000 shows as 2.5 lakh.
@Pipe({ name: 'lakh', standalone: true })
export class LakhPipe implements PipeTransform {
transform(rupees: number): string {
return (rupees / 100000).toFixed(1) + ' lakh';
}
}
// template: {{ 250000 | lakh }}
// shows: 2.5 lakh14. What kinds of directives does Angular have?
There are three kinds. A component is a directive with its own template. A structural directive adds or removes elements, like *ngIf and *ngFor, where the asterisk is shorthand for an ng-template; in Angular 17 and 18 the built-in @if and @for cover most of these needs. An attribute directive changes the look or behaviour of an existing element, like ngClass, ngStyle or a custom highlight directive. A custom attribute directive usually listens to host events and changes the host element.
@Directive({ selector: '[appHighlight]', standalone: true })
export class HighlightDirective {
private el = inject(ElementRef);
@HostListener('mouseenter') onEnter() {
this.el.nativeElement.style.background = '#fff3bf';
}
@HostListener('mouseleave') onLeave() {
this.el.nativeElement.style.background = '';
}
}15. How do you set up basic routing in a standalone Angular app?
You define a Routes array that maps paths to components and pass it to provideRouter in the app config. A path can have parameters such as jobs/:id, and the wildcard path, two asterisks, catches unknown URLs, so it must come last because the router uses the first route that matches. In templates you navigate with the routerLink directive, highlight the current link with routerLinkActive, and show the matched component inside the router-outlet element. In a standalone component, import RouterLink and RouterOutlet.
// app.routes.ts
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'jobs/:id', component: JobDetailComponent },
{ path: '**', component: NotFoundComponent },
];
// app.component.html
// <a routerLink="/jobs/42" routerLinkActive="active">View</a>
// <router-outlet />16. What is the difference between template-driven and reactive forms?
In template-driven forms you build the form in the HTML with ngModel and FormsModule, and Angular creates the form model for you. They suit small forms like a newsletter box. In reactive forms you build the model in the class with FormGroup and FormControl, bind it with ReactiveFormsModule, and get synchronous access to values, typed controls since Angular 14, and valueChanges streams. They suit complex, dynamic or heavily validated forms and are easier to unit test. Pick one style per form and do not mix ngModel with formControlName.
17. How is an Observable different from a Promise?
A Promise represents a single future value, starts its work as soon as it is created and cannot be cancelled. An Observable can emit zero, one or many values over time, does nothing until someone subscribes, and can be cancelled by unsubscribing. RxJS operators such as map, filter and switchMap let you transform those streams. HttpClient returns Observables, which surprises freshers: if you call http.post and never subscribe, no request is sent at all. The async pipe subscribes for you in templates.
// No request goes out: nobody subscribed
this.http.post('/api/applications', form);
// The request is sent when you subscribe
this.http.post('/api/applications', form)
.subscribe(() => this.submitted = true);18. What is a signal in Angular?
A signal is a wrapper around a value that tells Angular when the value changes. You create one with signal(initial), read it by calling it like a function, and change it with set or update. A computed signal derives a value from other signals and recalculates only when they change. When a template reads a signal, Angular knows exactly which component needs updating. signal and computed are stable from Angular 17. A common mistake is leaving out the brackets in a template, which prints the signal itself instead of its value.
export class CartComponent {
qty = signal(1);
price = signal(499);
total = computed(() => this.qty() * this.price());
add() {
this.qty.update(q => q + 1);
}
}
// template: {{ qty() }} x {{ price() }} = {{ total() }}19. How do you call a REST API with HttpClient?
First register the client with provideHttpClient() in the app config; in Angular 18 the old HttpClientModule is deprecated. Then inject HttpClient, usually inside a service, and call get, post, put or delete with a type for the response. The call returns an Observable, so either subscribe or let the async pipe handle subscribing and cleanup in the template. HttpClient parses JSON for you and reports non-2xx responses as errors, which you handle with catchError or the error callback.
@Component({
selector: 'app-jobs',
standalone: true,
imports: [AsyncPipe],
template: `
@if (jobs$ | async; as jobs) {
@for (job of jobs; track job.id) {
<p>{{ job.title }}</p>
}
}
`,
})
export class JobsComponent {
jobs$ = inject(HttpClient).get<Job[]>('/api/jobs');
}20. Which Angular CLI commands should every developer know?
ng new creates a project; in Angular 17 it is standalone by default and asks whether to set up SSR. ng serve starts a dev server with live reload, and new projects build with the esbuild-based application builder. ng generate, or ng g, creates components, services, pipes, guards and more with the right files. ng build makes a production build by default, ng test runs unit tests with Karma and Jasmine in a default project, ng add installs a library with its setup, and ng update upgrades Angular and runs its migrations.
Ready to check yourself? Take the free Angular mock test
Angular interview questions: Medium questions (1-3 years)
20 questions21. When do you use computed and when do you use effect?
Use computed for any value that can be derived from other signals, such as a filtered job list. It is pure, lazy and cached, and it recalculates only when a signal it read changes. Use effect for side effects that must happen when signals change: saving to localStorage, logging, or syncing with a chart library. Effects run at least once, need an injection context and were still developer preview in Angular 17 and 18, where writing signals inside them needed allowSignalWrites. Copying one signal into another with an effect is a classic mistake; use computed.
export class FilterComponent {
city = signal('Bengaluru');
jobs = signal<Job[]>([]);
cityJobs = computed(() =>
this.jobs().filter(j => j.city === this.city()));
constructor() {
effect(() => {
localStorage.setItem('city', this.city());
});
}
}22. What are signal inputs, model() and output() in Angular 17?
Angular 17.1 added input(), which makes an input a read-only signal; input.required() forces the parent to bind it. Angular 17.2 added model() for two-way binding, and 17.3 added output() as a lighter replacement for @Output with EventEmitter. They were developer preview in 17 and 18. Because inputs are signals, you can build computed values on them instead of writing ngOnChanges. The child cannot set an input signal; if it needs to change the value and tell the parent, use model() and bind it with [( )].
export class RatingComponent {
jobId = input.required<number>();
max = input(5);
value = model(0); // parent: [(value)]="score"
rated = output<number>();
stars = computed(() => Array(this.max()).fill(0));
pick(n: number) {
this.value.set(n);
this.rated.emit(n);
}
}23. Explain switchMap, mergeMap, concatMap and exhaustMap with real examples.
All four map each value to an inner Observable, such as an HTTP call, and differ in what happens when a new value arrives while the last call is still running. switchMap cancels the old call, which suits type-ahead search. mergeMap runs all calls in parallel, which suits independent uploads. concatMap queues them in order, which suits saving edits in sequence. exhaustMap ignores new values until the current call ends, which suits a login button clicked twice. Using mergeMap for search can show results of an older query last.
results$ = this.searchBox.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.jobs.search(term)),
);
// three searches typed quickly, each reply slow:
// switchMap -> only the last reply arrives
// mergeMap -> all three replies arrive
// exhaustMap -> only the first reply arrives24. How do you unsubscribe from Observables in Angular, and when is it really needed?
It matters for long-lived streams: interval, router events, fromEvent on window, WebSockets and Subjects in root services. Left open, they keep the component in memory and run handlers after it is gone. The best options are the async pipe, toSignal, and takeUntilDestroyed from @angular/core/rxjs-interop (Angular 16+), which needs an injection context or a DestroyRef. HttpClient calls complete after one response, so they do not leak, but unsubscribing early cancels the request. Avoid nested subscribe calls; use operators.
export class TickerComponent implements OnInit {
private destroyRef = inject(DestroyRef);
seconds = 0;
ngOnInit() {
interval(1000).pipe(
takeUntilDestroyed(this.destroyRef), // ngOnInit: pass the ref
).subscribe(n => this.seconds = n);
}
}25. What is the difference between Subject, BehaviorSubject and ReplaySubject?
All three are Observables you can push values into with next. A plain Subject has no current value, so a late subscriber only sees values sent after it subscribed, which suits events like a logout click. A BehaviorSubject needs an initial value and gives its latest value to every new subscriber at once, which suits state such as the cart count. A ReplaySubject replays the last n values to new subscribers. Keep the subject private in a service and expose asObservable so components cannot call next.
@Injectable({ providedIn: 'root' })
export class CartStore {
private count$ = new BehaviorSubject<number>(0);
readonly count = this.count$.asObservable();
add() {
this.count$.next(this.count$.value + 1);
}
}26. When does an OnPush component get checked for changes?
With the default strategy Angular checks a component on every change detection pass. With OnPush it checks the component only when it is marked dirty: an input receives a new value or object reference, an event bound in its template or a child fires, the async pipe receives a value, a signal read in its template changes, or code calls markForCheck. Mutating an object that was passed as an input, or changing a plain field from a setTimeout, does not mark it, so the view stays stale. OnPush works best with immutable data or signals.
27. An OnPush child does not update when the parent pushes a new job into the array. Why, and how do you fix it?
OnPush compares input values by reference. push changes the contents of the same array, so the [jobs] binding still holds the same reference and the child is not marked for checking. The fix is to create a new array, for example with the spread operator, or to keep the list in a signal and use update with a new array. It sometimes seems to work when the click happened inside the child, because an event in a component marks that component dirty, which hides the real bug until later.
// parent template: <app-job-list [jobs]="jobs" />
addJob(job: Job) {
this.jobs.push(job); // child (OnPush) stays stale
}
addJobFixed(job: Job) {
this.jobs = [...this.jobs, job]; // new reference: child updates
}
// with a signal
// this.jobs.update(list => [...list, job]);28. What causes ExpressionChangedAfterItHasBeenCheckedError, and how do you fix it properly?
In development mode Angular runs an extra check after each change detection pass to confirm that no binding changed while it was being checked. If one did, it throws NG0100. Typical causes are a child setting parent state in ngAfterViewInit, a getter that returns a new value each time such as Math.random(), or a service updated during rendering. Production builds skip the check, but the screen can be out of date. Fix the data flow by computing the value earlier, using a computed signal or letting the parent own the state; wrapping it in setTimeout only hides it.
29. How do you write a route guard in Angular 17, and what should it return?
Write it as a function of type CanActivateFn and use inject() inside it to get services. Return true to allow navigation, or a UrlTree from router.createUrlTree to redirect, for example to the login page with a returnUrl. Returning a UrlTree is better than calling router.navigate and returning false, because the router handles it as one clean redirect. Guards can also return an Observable or Promise of these. Remember that guards only protect navigation in the browser; the API must still check permissions.
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
return auth.isLoggedIn()
? true
: router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url },
});
};
// { path: 'applications', component: ApplicationsComponent,
// canActivate: [authGuard] }30. Going from /jobs/1 to /jobs/2 keeps showing job 1. What is wrong?
When only the route parameter changes, the router reuses the same component instance, so the constructor and ngOnInit do not run again. Code that read route.snapshot.paramMap once keeps the old id. Read the parameters as a stream instead: pipe paramMap through switchMap to load the job each time the id changes. From Angular 16 you can also enable withComponentInputBinding() in provideRouter, so the id arrives as an input you can react to. This bug often appears with related-job links on a detail page.
export class JobDetailComponent {
private route = inject(ActivatedRoute);
private jobs = inject(JobService);
// Bug: read once, never updates
// id = this.route.snapshot.paramMap.get('id');
job$ = this.route.paramMap.pipe(
map(p => Number(p.get('id'))),
switchMap(id => this.jobs.getJob(id)),
);
}31. How do you lazy load routes in a standalone Angular app?
Use loadComponent for a single standalone component or loadChildren for a group of child routes, both with a dynamic import. The build puts that code in a separate chunk that the browser downloads only on the first visit to the route, which shrinks the initial bundle. You can add withPreloading(PreloadAllModules) to fetch lazy chunks in the background after the app loads. A common mistake is importing the lazy component normally somewhere else, which pulls it back into the main bundle.
export const routes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'profile',
loadComponent: () => import('./profile/profile.component')
.then(m => m.ProfileComponent),
},
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes')
.then(m => m.ADMIN_ROUTES),
},
];32. What are @defer blocks, and when would you use them?
@defer, added in Angular 17 (developer preview, stable in 18), lazy loads part of a template, together with its components, directives and pipes, when a trigger fires: on idle (the default), on viewport, on interaction, on hover, on timer or a when condition. @placeholder shows before loading, @loading during it and @error if the chunk fails. Use it for heavy widgets below the fold, such as charts. The deferred components must be standalone and not used elsewhere in the same component, or they are loaded eagerly.
@defer (on viewport; prefetch on idle) {
<app-hiring-chart [data]="stats()" />
} @placeholder (minimum 300ms) {
<div class="chart-box">Chart loads when you scroll here</div>
} @loading (after 100ms; minimum 500ms) {
<app-spinner />
} @error {
<p>Could not load the chart.</p>
}33. How do you write an HTTP interceptor that adds an auth token?
In Angular 15 and later, write a function of type HttpInterceptorFn and register it with provideHttpClient(withInterceptors([...])). HttpRequest objects are immutable, so you clone the request with the new header and pass it to next. Interceptors run in the order listed for the request and in reverse order for the response. Attach the token only to your own API URLs, or it leaks to third-party services. Older class-based interceptors still work if you add withInterceptorsFromDi().
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).token();
if (!token || !req.url.startsWith('/api/')) {
return next(req);
}
return next(req.clone({
setHeaders: { Authorization: `Bearer ${token}` },
}));
};
// app.config.ts
// provideHttpClient(withInterceptors([authInterceptor]))34. Why does a type-ahead search stop working after one failed API call?
An error ends an Observable. If catchError sits on the outer pipe, after switchMap, the first failed request errors the whole stream; catchError replaces it with a fallback that completes, so later keystrokes are ignored. Put catchError inside the switchMap, on the inner HTTP Observable, so only that one request is replaced, for example by an empty list and an error message, and the outer stream keeps listening. You can also add retry with a count and delay for flaky mobile networks before falling back.
results$ = this.term$.pipe(
debounceTime(300),
switchMap(term =>
this.jobs.search(term).pipe(
catchError(() => {
this.error.set('Search failed, please try again');
return of([]);
}),
),
),
);35. How do you build a typed reactive form with validation?
Inject a form builder, build a FormGroup with initial values and validators, bind it with formGroup and formControlName from ReactiveFormsModule, and handle ngSubmit. Since Angular 14 forms are typed, so the value has real types. NonNullableFormBuilder makes reset() return controls to their initial values instead of null. Before submitting, call markAllAsTouched() so all errors show. Note that form.value leaves out disabled controls, while getRawValue() includes them, a frequent source of missing fields.
export class ApplyComponent {
private fb = inject(NonNullableFormBuilder);
form = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
experience: [0, [Validators.min(0), Validators.max(40)]],
});
submit() {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
const data = this.form.getRawValue();
}
}36. How do you write a custom validator, including one that compares two fields?
A validator is a function that receives a control and returns null when valid or an errors object such as { passwordsMismatch: true }. For a rule across fields, put the validator on the FormGroup so it can read both controls, and check the error on the group in the template. Async validators, like checking whether an email is already registered, return an Observable or Promise, run only after the sync validators pass and set the status to PENDING. Use updateOn: blur so they do not fire on every keystroke.
export const passwordsMatch: ValidatorFn = (group) => {
const pass = group.get('password')?.value;
const confirm = group.get('confirm')?.value;
return pass === confirm ? null : { passwordsMismatch: true };
};
form = this.fb.group(
{ password: ['', Validators.minLength(8)], confirm: [''] },
{ validators: passwordsMatch },
);
// template: @if (form.hasError('passwordsMismatch')) { ... }37. What changes when you add a service to a component providers array instead of providedIn: root?
providedIn: root gives one instance for the whole app. Listing the service in a component providers array creates a new instance for each instance of that component, shared with its child components and destroyed with it, and the service ngOnDestroy runs then. This is right for per-instance state, such as two job editors on one page with separate drafts. It is also a common bug: listing a root service in a component providers array by accident creates a second copy, and shared state stops being shared.
38. What is the difference between @ViewChild and @ContentChild?
@ViewChild finds an element, directive or child component in the component own template, and it is ready in ngAfterViewInit. @ContentChild finds something the parent projected into the component through ng-content, and it is ready in ngAfterContentInit. With static: true a ViewChild that is not inside @if or another structural block is available already in ngOnInit. Angular 17.2 added signal versions, viewChild() and contentChild(). A frequent mistake is using a ViewChild in ngOnInit and getting undefined.
@Component({
selector: 'app-card',
standalone: true,
template: `
<input #search />
<ng-content></ng-content>
`,
})
export class CardComponent implements AfterViewInit {
@ViewChild('search') search!: ElementRef<HTMLInputElement>;
@ContentChild(CardTitleDirective) title?: CardTitleDirective;
ngAfterViewInit() {
this.search.nativeElement.focus();
}
}39. How do you unit test a standalone component with TestBed?
Configure the testing module with the standalone component in imports, not declarations, then create it with TestBed.createComponent. Set inputs with fixture.componentRef.setInput, which works for decorator and signal inputs and also marks OnPush components dirty. Call fixture.detectChanges() to render, then query the DOM and assert on what the user would see. Replace real services with stubs through providers so the test does not call an API. In Angular 17 and 18 a default CLI project runs these tests with Karma and Jasmine.
describe('JobCardComponent', () => {
let fixture: ComponentFixture<JobCardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [JobCardComponent],
}).compileComponents();
fixture = TestBed.createComponent(JobCardComponent);
});
it('shows the job title', () => {
fixture.componentRef.setInput('title', 'QA Engineer');
fixture.detectChanges();
const h3 = fixture.nativeElement.querySelector('h3');
expect(h3.textContent).toContain('QA Engineer');
});
});40. How do you test a service that calls HttpClient?
Provide provideHttpClient() followed by provideHttpClientTesting(), then inject the service and HttpTestingController; in Angular 18 the older HttpClientTestingModule is deprecated. Call the service method and subscribe, then use expectOne to find the request by URL, check its method or body, and answer it with flush. No real network call happens. In afterEach call verify(), which fails the test if an unexpected request is still waiting. Forgetting to subscribe means expectOne finds nothing, because no request was made.
beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideHttpClient(), provideHttpClientTesting()],
});
service = TestBed.inject(JobService);
httpMock = TestBed.inject(HttpTestingController);
});
it('loads jobs for a city', () => {
service.getJobs('Pune').subscribe(jobs => {
expect(jobs.length).toBe(1);
});
const req = httpMock.expectOne('/api/jobs?city=Pune');
expect(req.request.method).toBe('GET');
req.flush([{ id: 1, title: 'Tester', city: 'Pune' }]);
});
afterEach(() => httpMock.verify());Ready to check yourself? Take the free Angular mock test
Angular interview questions: Hard questions (experienced)
20 questions41. How does change detection work with Zone.js, and what changes with zoneless change detection in Angular 18?
Zone.js patches browser async APIs such as events, timers, promises and XHR. When a task inside the Angular zone finishes, Angular calls ApplicationRef.tick(), which checks the tree from the root: every Default component and any dirty OnPush one. That is simple but runs after events that change nothing. Angular 18 added experimental zoneless mode with provideExperimentalZonelessChangeDetection(): Angular checks only after a notification such as a signal change, markForCheck, the async pipe or a template event. A plain field set in setTimeout is not shown until something else triggers a check.
// app.config.ts (Angular 18, experimental)
export const appConfig: ApplicationConfig = {
providers: [
provideExperimentalZonelessChangeDetection(),
provideRouter(routes),
],
};
// also remove "zone.js" from polyfills in angular.json42. How do signals track dependencies, and why does mutating an array inside a signal not update the UI?
While a computed or a template runs, every signal it reads is recorded as a dependency, and the set is rebuilt on each run, so only signals read last time count. A change marks dependents dirty, but a computed recalculates only when someone reads it, and it caches the result. This push-then-pull model is glitch-free. Signals compare values with Object.is, so pushing into the same array and setting that reference again looks unchanged. Always set a new array or object, or pass a custom equal function.
const skills = signal<string[]>(['Angular']);
const count = computed(() => skills().length);
count(); // 1
skills().push('RxJS'); // in-place mutation
skills.set(skills()); // same reference: ignored
count(); // still 1 (cached)
skills.update(s => [...s, 'NgRx']);
count(); // 343. How do toSignal and toObservable work, and what are their pitfalls?
Both come from @angular/core/rxjs-interop, developer preview in Angular 17 and 18. toSignal subscribes at once and unsubscribes when its injection context is destroyed. Without initialValue its type includes undefined; requireSync: true suits a BehaviorSubject that emits synchronously. Call it once in a field, not in a method or getter, or each call opens a new subscription. toObservable uses an effect, so it emits asynchronously and only the latest of several quick sets. Both need an injection context or an injector option.
export class SearchComponent {
private jobs = inject(JobService);
term = signal('');
results = toSignal(
toObservable(this.term).pipe(
debounceTime(300),
switchMap(t => this.jobs.search(t)),
),
{ initialValue: [] as Job[] },
);
}44. How would you choose a state management approach for a large Angular app?
Start simple: for most features a service per domain that keeps private writable signals, exposes read-only ones with asReadonly(), derives values with computed and changes state only through methods is enough and easy to test. Move to NgRx Store when many teams share complex state, side effects are hard to follow and you need Redux DevTools, accepting the extra boilerplate. NgRx SignalStore sits in between. Decide per feature, keep server cache separate from UI state, and never let components write shared state directly.
@Injectable({ providedIn: 'root' })
export class ShortlistStore {
private readonly _ids = signal<number[]>([]);
readonly ids = this._ids.asReadonly();
readonly count = computed(() => this._ids().length);
toggle(id: number) {
this._ids.update(list => list.includes(id)
? list.filter(x => x !== id)
: [...list, id]);
}
}45. Explain the injector hierarchy in a standalone Angular app and how Angular resolves a dependency.
There are two trees. Element injectors come from providers on components and directives and follow the component tree. Environment injectors come from bootstrapApplication providers, the root injector, and route providers arrays, which create a child injector for that route branch. Angular searches element injectors upward from the requester, then the environment injectors up to root and platform, and throws NG0201 if nothing provides the token. The options optional, self, skipSelf and host on inject() change where the search starts or stops.
46. What are InjectionToken, useFactory and multi providers used for?
TypeScript interfaces vanish at runtime, so a config object cannot be injected by its type; you create an InjectionToken and provide a value for it. Providers can use useValue for constants, useClass to swap an implementation such as a custom ErrorHandler, useExisting for an alias, and useFactory to decide at runtime, with inject() allowed inside the factory. multi: true collects several providers for one token into an array, as APP_INITIALIZER and HTTP_INTERCEPTORS do. Mixing multi and normal providers for one token is an error.
export interface AppConfig { apiUrl: string; pageSize: number; }
export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');
export const appConfig: ApplicationConfig = {
providers: [
{ provide: APP_CONFIG,
useValue: { apiUrl: '/api', pageSize: 20 } },
{ provide: ErrorHandler, useClass: AppErrorHandler },
{
provide: Logger, // an abstract class as token
useFactory: () => isDevMode()
? new ConsoleLogger()
: new RemoteLogger(inject(HttpClient)),
},
],
};
// usage: private config = inject(APP_CONFIG);47. How would you refresh an expired token in an interceptor without firing many refresh calls?
Catch 401 responses in a functional interceptor, call the refresh endpoint, then retry the original request once with the new token. The trap is concurrency: if five requests fail together, a naive version sends five refresh calls. Keep one in-flight refresh Observable in the auth service, shared with shareReplay and cleared when done, so all failed requests wait for the same result. Skip the refresh endpoint to avoid a loop, and log the user out if refresh fails. Keeping the refresh token in an HttpOnly cookie limits XSS damage.
export const refreshInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
return next(req).pipe(
catchError((err: HttpErrorResponse) => {
if (err.status !== 401 || req.url.includes('/auth/refresh')) {
return throwError(() => err);
}
// refreshToken() returns one shared in-flight call
return auth.refreshToken().pipe(
switchMap(token => next(req.clone({
setHeaders: { Authorization: `Bearer ${token}` },
}))),
);
}),
);
};48. What is the difference between canMatch and canActivate, especially for lazy routes?
canActivate runs after the router has matched a route and decides whether navigation may continue. On a route with loadChildren it does not stop the download, because the router loads the child routes while matching. canMatch runs during matching: if it returns false the router skips that route and tries the next one, and its lazy code is never downloaded. That lets two routes share a path, such as dashboard, and load a different component for employers and job seekers. Neither replaces server-side authorization.
export const routes: Routes = [
{
path: 'dashboard',
canMatch: [() => inject(AuthService).isEmployer()],
loadComponent: () => import('./employer/dashboard.component')
.then(m => m.EmployerDashboardComponent),
},
{
path: 'dashboard',
loadComponent: () => import('./seeker/dashboard.component')
.then(m => m.SeekerDashboardComponent),
},
];49. The initial bundle of your Angular app is too large. How do you find the cause and reduce it?
Measure first: build with source maps and inspect the output with source-map-explorer, or use --stats-json with an esbuild analyzer when on the application builder. Set budgets in angular.json so the build warns or fails when the initial bundle grows. Then split by route with loadComponent and loadChildren, use @defer for heavy widgets, and preload chunks after the first paint. Watch for libraries imported whole instead of per function, CommonJS packages the CLI warns about, large locale data and big icon sets.
// angular.json -> configurations.production
"budgets": [
{ "type": "initial",
"maximumWarning": "500kB", "maximumError": "1MB" },
{ "type": "anyComponentStyle",
"maximumWarning": "4kB", "maximumError": "8kB" }
]
// app.config.ts
provideRouter(routes, withPreloading(PreloadAllModules))50. A page listing 2,000 jobs lags when the user types in a filter box. How do you fix it?
Profile first with the Angular DevTools profiler to see how many change detection cycles run and which components are slow. Then make rows OnPush with immutable data or signals, track rows by id, and move filtering into a computed so it runs once per change, not per check. Debounce the box. Render only the visible rows with CDK virtual scrolling or pagination. Run high-frequency listeners such as mousemove or scroll outside Angular with NgZone.runOutsideAngular and re-enter only when state changes.
51. Why is calling a method in a template a performance concern, and what are the alternatives?
A method call in a binding runs on every change detection pass that checks the component, for every row. That is fine for a cheap lookup but costly when it sorts, filters or formats a large array. If it returns a new array or object each time, it also defeats OnPush in child components and can trigger NG0100 errors in development. A pure pipe runs again only when its arguments change, and a computed signal caches its result until a signal it depends on changes. Reading a signal is a function call too, but it is cheap.
<!-- runs on every check, for every row -->
<li>{{ formatExperience(job) }}</li>
<!-- pure pipe: runs only when its arguments change -->
<li>{{ job.minYears | yearsRange: job.maxYears }}</li>
<!-- computed signal: cached until jobs() changes -->
<p>{{ totalOpenings() }} openings</p>52. How do you build a custom form control that works with formControlName?
Implement ControlValueAccessor and register the component as NG_VALUE_ACCESSOR with useExisting, forwardRef and multi: true. Angular calls writeValue to push a value in, gives you change and touched callbacks through registerOnChange and registerOnTouched, and calls setDisabledState when the control is disabled. Call the change callback when the user picks a value and the touched callback on blur or pick, or validation messages never appear. If the control is OnPush, call markForCheck in writeValue so values set from code show up.
@Component({
selector: 'app-star-rating',
standalone: true,
template: `
@for (s of stars; track s) {
<button type="button" [class.on]="value >= s"
[disabled]="disabled" (click)="pick(s)">*</button>
}`,
providers: [{ provide: NG_VALUE_ACCESSOR, multi: true,
useExisting: forwardRef(() => StarRatingComponent) }],
})
export class StarRatingComponent implements ControlValueAccessor {
stars = [1, 2, 3, 4, 5];
value = 0;
disabled = false;
private onChange = (v: number) => {};
private onTouched = () => {};
writeValue(v: number) { this.value = v ?? 0; }
registerOnChange(fn: (v: number) => void) { this.onChange = fn; }
registerOnTouched(fn: () => void) { this.onTouched = fn; }
setDisabledState(d: boolean) { this.disabled = d; }
pick(s: number) {
this.value = s;
this.onChange(s);
this.onTouched();
}
}53. Two components use the same cities$ Observable and the API is called twice. How do you share one request?
HttpClient Observables are cold: each subscriber, including each async pipe, runs its own request. Add shareReplay so all subscribers share one execution and late subscribers get the cached last value. Use the config form with bufferSize 1 and refCount true: once the HTTP call has completed, late subscribers still get the cached value without a new request. With a source that never completes, shareReplay(1) keeps it subscribed after every component has gone, which is a leak; refCount: true closes it when the last subscriber leaves.
@Injectable({ providedIn: 'root' })
export class CityService {
private http = inject(HttpClient);
readonly cities$ = this.http.get<City[]>('/api/cities').pipe(
shareReplay({ bufferSize: 1, refCount: true }),
);
}54. When do you use forkJoin and when combineLatest, and what are the traps?
forkJoin waits until every source completes and emits once with the last value of each, which suits loading the profile and applications in parallel. Traps: if one source fails, the whole forkJoin fails unless each call has its own catchError; a source that never completes, such as valueChanges or a BehaviorSubject, means it never emits; a source that completes empty makes it complete with nothing. combineLatest emits each time any source emits, once all have emitted once, which suits live filters such as city plus experience.
// one-shot parallel load
forkJoin({
profile: this.api.getProfile(),
applications: this.api.getApplications(),
}).subscribe(({ profile, applications }) => { /* render */ });
// live filters: re-query on every change
combineLatest([this.city$, this.minExp$]).pipe(
switchMap(([city, exp]) => this.api.search(city, exp)),
);55. What changed for SSR and hydration in Angular 17 and 18, and what breaks most often?
Angular 17 replaced Angular Universal with @angular/ssr, available through ng new --ssr or ng add @angular/ssr, and made non-destructive hydration stable with provideClientHydration(): the browser reuses server HTML instead of rebuilding it, and GET requests made on the server are cached so they are not repeated. Angular 18 added event replay as developer preview. Common breaks: window or localStorage touched on the server, DOM changed outside Angular causing hydration mismatches, and timers that keep the app from becoming stable.
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(withFetch()),
provideClientHydration(withEventReplay()), // replay: Angular 18
],
};
export class ShortlistComponent {
constructor() {
afterNextRender(() => {
// runs only in the browser, so localStorage is safe here
this.restore(localStorage.getItem('shortlist'));
});
}
}56. How does Angular protect against XSS and CSRF, and how do developers break that protection?
Angular treats every bound value as untrusted. Interpolation escapes text, and binding to innerHTML or a URL property is sanitized, so script tags are stripped and javascript: URLs are made harmless. Developers break this with DomSanitizer bypassSecurityTrustHtml on user content such as a job description, or by writing to nativeElement.innerHTML directly. For CSRF, HttpClient reads the XSRF-TOKEN cookie and sends an X-XSRF-TOKEN header on mutating requests to relative URLs only, and the server must check it. Tokens in localStorage can be read by any XSS.
// safe: Angular sanitizes the HTML
// <div [innerHTML]="job.description"></div>
// dangerous on user content: switches sanitizing off
this.html = this.sanitizer.bypassSecurityTrustHtml(job.description);
// also bypasses Angular completely
this.el.nativeElement.innerHTML = job.description;57. How do you test code that uses debounceTime or other timers?
Wrap the test in fakeAsync so timers run on virtual time, change the input, and call tick to move the clock forward. That proves a 300 ms debounce sends one search after the user stops typing, without the test actually waiting. flush runs all pending timers, and the test fails if periodic timers are still queued at the end unless you call discardPeriodicTasks. For complex streams, RxJS marble tests with TestScheduler are more precise, and CDK component harnesses keep tests stable when markup changes.
it('searches once after typing stops', fakeAsync(() => {
const search = jasmine.createSpy('search').and.returnValue(of([]));
TestBed.configureTestingModule({
imports: [SearchComponent],
providers: [{ provide: JobService, useValue: { search } }],
});
const fixture = TestBed.createComponent(SearchComponent);
fixture.detectChanges();
fixture.componentInstance.term.setValue('ang');
tick(100);
fixture.componentInstance.term.setValue('angular');
tick(300);
expect(search).toHaveBeenCalledOnceWith('angular');
}));58. How would you migrate a large NgModule-based app to standalone components and the new control flow?
Upgrade one major version at a time with ng update, then run the official schematics: ng generate @angular/core:standalone in its three modes in order (convert declarations, remove unneeded NgModules, switch to bootstrapApplication), and ng generate @angular/core:control-flow to turn *ngIf, *ngFor and ngSwitch into @if, @for and @switch. Go feature by feature with tests after each step; standalone components and NgModules can import each other, so both styles can live together. Wrap module-only libraries with importProvidersFrom.
ng update @angular/core@17 @angular/cli@17
ng generate @angular/core:standalone
ng generate @angular/core:control-flow
ng test
ng update @angular/core@18 @angular/cli@1859. What does the AOT compiler do, and why should strictTemplates stay on?
Ahead-of-time compilation, the default since Angular 9 with the Ivy engine, turns templates into JavaScript instructions at build time. The browser then does not need the template compiler, startup is faster, and template errors appear at build time. Ivy compiles each component from its own metadata, which helps incremental builds and lazy loading of single components. strictTemplates, on in new projects, type-checks bindings: input types, null checks and $event types. Silencing errors with $any() turns build errors back into runtime bugs.
60. How do you find and fix memory leaks in an Angular application?
Signs are memory that grows each time you move between two routes, or an API called twice after revisiting a page. Reproduce it, take heap snapshots in Chrome DevTools before and after ten navigations, and look for component instances and detached DOM nodes that should be gone. Usual causes are subscriptions to long-lived streams, listeners on window or document, setInterval, and chart or map widgets that are never destroyed. Fix with takeUntilDestroyed or the async pipe, DestroyRef.onDestroy, and the unlisten function from Renderer2.listen.
Practice questions for November 2026
New every month3 questions from the Angular mock test, the way they appear in it. A new set comes on the 1st of every month. Pick your answer, then open it.
-
Can you call set() on a signal created with computed()?
- Only when it was created with { writable: true } as the second argument
- Yes, calling set() replaces the computed's formula with the plain value given
- Yes, but only inside ngOnInit, after the component's inputs have been set
- No, a computed signal is read-only; change the signals it depends on instead
Show the answer
Answer: No, a computed signal is read-only; change the signals it depends on instead. computed() returns a Signal, not a WritableSignal, so it has no set() or update(). Its value always comes from its function, which keeps derived state in sync with its sources.
-
In RxJS 7, what does this code log, in order?
from([30, 10]).pipe( mergeMap(d => timer(d).pipe(map(() => d))) ).subscribe(v => console.log(v));- 30, 10
- 10, 30
- 10
- 30
Show the answer
Answer: 10, 30. mergeMap subscribes to both inner timers at once. The 10 ms timer fires first, so 10 is logged before 30. mergeMap does not keep the order of the source values.
-
A list of rows, each with its own open/closed dropdown state, uses track $index. The first row is removed. What can go wrong?
- Angular throws a duplicate key error, because two rows briefly share the same index
- The whole list is re-created from scratch, so focus is lost but each row keeps its state
- Views are matched by position, so remaining rows reuse other items' views and show the wrong state
- Nothing can go wrong, because $index is always the best and safest track key for any list of components
Show the answer
Answer: Views are matched by position, so remaining rows reuse other items' views and show the wrong state. With track $index, the view at index 0 is kept and gets the data of the old second item, so its local state now belongs to the wrong row. A stable id makes Angular remove exactly the deleted row's view.
Ready to apply?
There are 793 open jobs asking for Angular on CoreHRx right now. Create a free profile once and apply with it.