Angular Interview Questions
💡 Click Show Answer to generate an AI-powered answer instantly.
What is FormBuilder in Angular?
How does Angular handle HTTP requests?
What are Observables in Angular?
Observables are a fundamental part of Angular applications, extensively used for handling asynchronous data streams. They are a powerful feature from the RxJS library that Angular leverages for various tasks, including HTTP requests, event handling, and reactive programming.
What are Observables?
An Observable is a declarative way to handle asynchronous operations. It represents a stream of data or events that can be observed over time. Unlike Promises, which handle a single future value, Observables can emit multiple values over a period, making them suitable for continuous data streams.
Key Characteristics
- Lazy Execution: Observables do not execute until a consumer (subscriber) subscribes to them.
- Multiple Values: They can emit zero, one, or multiple values over time, unlike Promises which resolve with a single value.
- Push System: Observables push data to their subscribers, rather than subscribers pulling data.
- Cancellation: Subscriptions can be easily cancelled, preventing memory leaks and unnecessary work.
Core Components
Observable
The Observable itself is a function that connects an observer to a stream of values or events. It defines how to produce and push values to observers.
Observer (Subscriber)
An Observer is a collection of callbacks that knows how to react to values delivered by the Observable. It defines three methods: next() for handling emitted values, error() for handling errors, and complete() for handling the completion of the stream.
Subscription
A Subscription is the object returned when you subscribe to an Observable. It represents the ongoing execution of the Observable and has a unsubscribe() method to stop receiving notifications and clean up resources.
Operators
Operators are pure functions that allow you to compose complex asynchronous logic in a declarative manner. They take an Observable as input and return a new Observable, enabling transformations, filtering, and combining of data streams (e.g., map, filter, debounceTime).
Example: Creating and Subscribing
import { Observable } from 'rxjs';
// Create an Observable that emits numbers 1, 2, 3
const myObservable = new Observable<number>(subscriber => {
subscriber.next(1);
subscriber.next(2);
subscriber.next(3);
setTimeout(() => {
subscriber.next(4);
subscriber.complete(); // Signal that the stream has finished
}, 1000);
});
// Subscribe to the Observable
console.log('Before subscribe');
const subscription = myObservable.subscribe({
next(x) { console.log('Got value ' + x); },
error(err) { console.error('Something wrong occurred: ' + err); },
complete() { console.log('Done'); }
});
console.log('After subscribe');
// To unsubscribe later (e.g., in ngOnDestroy in Angular components)
// subscription.unsubscribe();
Common Use Cases in Angular
- HTTP Requests: The
HttpClientin Angular returns Observables, making it easy to handle responses, errors, and chain requests. - Event Handling: Listening to DOM events (e.g.,
fromEventoperator). - Reactive Forms: Tracking value changes (
valueChanges) and status changes (statusChanges) of form controls and groups. - Router Events: Subscribing to router events to perform actions based on navigation lifecycle.
- State Management: In libraries like NgRx, Observables are central to managing application state.
What is RxJS and why is it used in Angular?
What is the difference between Promise and Observable?
In Angular and modern JavaScript applications, managing asynchronous operations is crucial. Promises and Observables are two fundamental constructs designed to handle asynchronous data, but they differ significantly in their capabilities and use cases.
Understanding Promises
A Promise is an object representing the eventual completion or failure of an asynchronous operation. It can only emit a single value and then either resolve successfully or reject with an error. Once settled (resolved or rejected), a Promise cannot change its state.
- Single Value: Promises emit only one value (or an error) and then complete.
- Eager Execution: Promises execute immediately when defined, even before
.then()is called. - Not Cancelable: Once a Promise starts, there's no built-in way to cancel its execution.
- Error Handling: Errors are handled via
.catch()or the second callback of.then().
const myPromise = new Promise((resolve, reject) => {
console.log('Promise executed immediately');
setTimeout(() => {
resolve('Promise resolved with a single value!');
}, 1000);
});
myPromise.then(value => console.log(value)).catch(error => console.error(error));
Understanding Observables
An Observable is a stream of data that can emit multiple values over time. It represents a sequence of items pushed asynchronously. Observables are a core part of ReactiveX (RxJS) and are widely used in Angular for handling events, HTTP requests, and other asynchronous operations.
- Multiple Values: Observables can emit zero, one, or multiple values over time.
- Lazy Execution: Observables only start executing when a consumer subscribes to them.
- Cancelable: Subscriptions can be unsubscribed from, stopping the Observable's execution and releasing resources.
- Richer Operators: RxJS provides a vast array of operators for transforming, filtering, and combining Observable streams.
- Error Handling: Errors are handled within the
errorcallback of thesubscribemethod.
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';
const myObservable = new Observable<string>(subscriber => {
console.log('Observable executed only when subscribed');
subscriber.next('First value');
setTimeout(() => subscriber.next('Second value'), 500);
setTimeout(() => {
subscriber.next('Third value');
subscriber.complete();
}, 1000);
});
const subscription = myObservable.subscribe({
next: value => console.log(value),
error: err => console.error(err),
complete: () => console.log('Observable completed')
});
// To demonstrate lazy execution, this won't log anything until subscribed
Key Differences: Promise vs. Observable
| Feature | Promise | Observable |
|---|---|---|
| Emits | Single value | Multiple values |
| Execution | Eager (starts immediately) | Lazy (starts on subscription) |
| Cancellation | Not cancelable | Cancelable via `unsubscribe()` |
| Operators | Limited (`.then()`, `.catch()`, `.finally()`) | Rich set of RxJS operators |
| Error Handling | Global/Chainable `.catch()` | Per subscriber, handled in `error` callback |
| Use Case | One-off async operations (e.g., HTTP GET) | Streams of events, long-running operations (e.g., HTTP PUT/POST, UI events) |
When to Use Which
Choosing between a Promise and an Observable depends on the specific requirements of your asynchronous operation.
Use Promises when:
- You need to handle a single asynchronous event.
- The operation is relatively simple and doesn't require complex data transformations or cancellations.
- You are integrating with older APIs or libraries that return Promises.
Use Observables when:
- You need to handle multiple asynchronous events over time (e.g., user input, real-time data).
- The operation needs to be cancelable (e.g., an ongoing HTTP request when a component is destroyed).
- You require powerful operators for transformation, filtering, debouncing, or combining streams.
- You are working extensively with Angular's
HttpClientor other reactive patterns.
In Angular, Observables are generally preferred due to their reactive nature and the rich RxJS ecosystem, offering more power and flexibility for complex asynchronous scenarios. However, Promises still have their place for simpler, one-off asynchronous tasks.
How do you handle errors in Angular HTTP calls?
Robust error handling is crucial for any web application, especially when dealing with asynchronous HTTP requests in Angular. It ensures a better user experience, helps in debugging, and prevents application crashes. Angular provides powerful tools, primarily through RxJS operators and HTTP Interceptors, to manage and respond to errors effectively.
Understanding HTTP Error Types
HTTP errors can broadly be categorized into client-side (network issues, request misconfigurations) and server-side (status codes 4xx, 5xx). Differentiating between these helps in providing relevant feedback and logging.
Using `catchError` for Localized Handling
The catchError RxJS operator is the primary tool for handling errors that occur within an Observable stream. It allows you to intercept an error, perform an action (like logging or transforming the error), and then return a new Observable or rethrow the error.
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class DataService {
private apiUrl = 'https://api.example.com/data';
constructor(private http: HttpClient) {}
getData(): Observable<any> {
return this.http.get<any>(this.apiUrl).pipe(
catchError(this.handleError)
);
}
private handleError(error: HttpErrorResponse) {
let errorMessage = '';
if (error.error instanceof ErrorEvent) {
// Client-side or network error occurred.
errorMessage = `Client-side error: ${error.error.message}`;
} else {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong.
errorMessage = `Server-side error: Status ${error.status}, Message: ${error.message}`;
}
console.error(errorMessage); // Log the error
return throwError(() => new Error('Something bad happened; please try again later.')); // Re-throw for component to handle
}
}
The `handleError` Function Explained
The handleError function is a common pattern to centralize the logic for processing different types of HTTP errors. It typically checks if the error is a client-side ErrorEvent or a server-side HttpErrorResponse and then logs the error and returns a user-friendly error message via throwError.
Global Error Handling with HTTP Interceptors
For a more centralized approach, HTTP Interceptors can catch and handle errors for all outgoing HTTP requests and incoming responses. This is ideal for logging errors, displaying global notifications, or refreshing authentication tokens.
import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe(
catchError((error: HttpErrorResponse) => {
let errorMessage = '';
if (error.error instanceof ErrorEvent) {
// Client-side error
errorMessage = `Client error: ${error.error.message}`;
} else {
// Server-side error
errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
}
console.error('Caught by Interceptor:', errorMessage); // Log the error globally
// Optionally display a toast or modal here
return throwError(() => new Error(errorMessage));
})
);
}
}
Remember to register your interceptor in the providers array of your AppModule:
import { HTTP_INTERCEPTORS } from '@angular/common/http';
// ...
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }
]
Displaying Errors to Users
Beyond logging, it's essential to provide meaningful feedback to users when an error occurs. The chosen method depends on the severity and context of the error.
- Toast notifications or snackbars for transient messages.
- Modals or dialogs for critical errors requiring user interaction.
- Inline error messages next to form fields for validation issues.
- Dedicated error pages for unrecoverable application states (e.g., 404, 500).
Retrying Failed Requests
For transient network issues, the retry or retryWhen RxJS operators can be used to automatically re-attempt a failed HTTP request a specified number of times before propagating the error.
import { catchError, retry } from 'rxjs/operators';
// ...
getDataWithRetry(): Observable<any> {
return this.http.get<any>(this.apiUrl).pipe(
retry(3), // Retry up to 3 times
catchError(this.handleError)
);
}
Explain Angular component lifecycle hooks in detail.
Angular components have a well-defined lifecycle managed by Angular itself. Lifecycle hooks allow developers to tap into key moments during a component's creation, update, and destruction, enabling fine-grained control over component behavior.
Overview
Angular provides a set of interfaces, known as lifecycle hooks, that you can implement in your components and directives. These interfaces declare methods that Angular calls at specific points in the component's lifecycle. By implementing these methods, you can execute custom logic at the right time, such as initializing data, performing DOM manipulation, or cleaning up resources.
Core Lifecycle Hooks
ngOnChanges
Called before ngOnInit (if the component has bound inputs) and whenever one or more data-bound input properties change. It receives a SimpleChanges object containing the current and previous property values. This hook is ideal for reacting to changes in input properties and performing actions based on those changes.
import { Component, OnChanges, Input, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-child',
template: '<p>Name: {{ name }}</p>'
})
export class ChildComponent implements OnChanges {
@Input() name: string;
ngOnChanges(changes: SimpleChanges): void {
if (changes['name']) {
console.log('Name changed from', changes['name'].previousValue, 'to', changes['name'].currentValue);
}
}
}
ngOnInit
Called once after the first ngOnChanges. Initializes the component after Angular sets the component's input properties and checks the first content. It's the most commonly used hook for initial setup, data fetching from a service, or complex initialization logic that doesn't rely on input changes.
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-my-component',
template: '<p>Component initialized!</p>'
})
export class MyComponent implements OnInit {
ngOnInit(): void {
console.log('ngOnInit: Component has been initialized.');
// Perform data fetching or other setup here
}
}
ngDoCheck
Called immediately after ngOnChanges and ngOnInit, and then after every subsequent ngOnChanges. It is also called after every change detection run, even if no inputs have changed. This hook allows you to implement your own custom change detection logic or to detect changes that Angular might not catch automatically (e.g., changes within objects or arrays passed as inputs without changing their reference). Use with caution as it can impact performance if not optimized.
import { Component, DoCheck } from '@angular/core';
@Component({
selector: 'app-do-check-component',
template: '<p>DoCheck component</p>'
})
export class DoCheckComponent implements DoCheck {
ngDoCheck(): void {
console.log('ngDoCheck: Always checking for changes.');
// Custom change detection logic here
}
}
ngAfterContentInit
Called once after Angular projects external content into the component's view (content projected via <ng-content>). This hook is useful for performing initialization logic after all projected content has been initialized. It's specifically for content that is 'transcluded' into the component.
import { Component, AfterContentInit, ContentChild, ElementRef } from '@angular/core';
@Component({
selector: 'app-content-parent',
template: `
<div #projectedContent>
<ng-content></ng-content>
</div>
`
})
export class ContentParentComponent implements AfterContentInit {
@ContentChild('projectedContent') projectedDiv: ElementRef;
ngAfterContentInit(): void {
console.log('ngAfterContentInit: Projected content initialized.', this.projectedDiv.nativeElement.textContent);
}
}
ngAfterContentChecked
Called after every ngDoCheck and after the content of the component has been checked. This hook is useful for performing actions that need to happen after Angular has checked the projected content for changes. It's triggered frequently, so use sparingly.
import { Component, AfterContentChecked } from '@angular/core';
@Component({
selector: 'app-content-checked-parent',
template: `<ng-content></ng-content>`
})
export class ContentCheckedParentComponent implements AfterContentChecked {
ngAfterContentChecked(): void {
console.log('ngAfterContentChecked: Projected content checked.');
}
}
ngAfterViewInit
Called once after Angular initializes the component's view and its child views. This hook is ideal for direct DOM manipulation, working with child components (using @ViewChild or @ViewChildren), or integrating third-party libraries that require access to the rendered view. It is important to note that content children are not available here, only view children.
import { Component, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
import { ChildComponent } from './child.component'; // Assuming you have a child component
@Component({
selector: 'app-view-init-component',
template: `
<p #myParagraph>This is a paragraph.</p>
<app-child-component></app-child-component>
`
})
export class ViewInitComponent implements AfterViewInit {
@ViewChild('myParagraph') paragraphElement: ElementRef;
@ViewChild(ChildComponent) childComponent: ChildComponent;
ngAfterViewInit(): void {
console.log('ngAfterViewInit: Component view and child views initialized.');
console.log('Paragraph text:', this.paragraphElement.nativeElement.textContent);
// this.childComponent.someMethod(); // Uncomment if ChildComponent has such a method
}
}
ngAfterViewChecked
Called after every ngDoCheck and after the component's view and child views have been checked. Similar to ngAfterContentChecked, but specifically for the component's own view and its children. It's suitable for performing actions that rely on the updated state of the view after every change detection cycle. Use with extreme care to avoid performance issues or infinite loops if you modify the view within this hook.
import { Component, AfterViewChecked } from '@angular/core';
@Component({
selector: 'app-view-checked-component',
template: `<p>View checked.</p>`
})
export class ViewCheckedComponent implements AfterViewChecked {
ngAfterViewChecked(): void {
console.log('ngAfterViewChecked: Component view and child views checked.');
}
}
ngOnDestroy
Called just before Angular destroys the component. This hook is crucial for cleanup logic, such as unsubscribing from observables, detaching event handlers, or clearing intervals/timers, to prevent memory leaks and ensure resources are properly released. It's guaranteed to be called once when the component is removed from the DOM.
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription, interval } from 'rxjs';
@Component({
selector: 'app-destroy-component',
template: '<p>Destroy me!</p>'
})
export class DestroyComponent implements OnInit, OnDestroy {
private intervalSubscription: Subscription;
ngOnInit() {
this.intervalSubscription = interval(1000).subscribe(val => console.log(val));
}
ngOnDestroy(): void {
console.log('ngOnDestroy: Component is being destroyed. Cleaning up resources.');
this.intervalSubscription.unsubscribe(); // Prevent memory leak
}
}
What is the difference between ngOnInit and constructor?
In Angular, both the constructor and ngOnInit are lifecycle hooks that allow you to execute code when a component or directive is initialized. However, they serve different purposes and are invoked at different stages of the component lifecycle.
The constructor
The constructor is a standard TypeScript feature (and JavaScript ES6+) used for class instantiation. It's the first method that gets called when a new instance of a class is created. Its primary purpose in Angular is for dependency injection.
When Angular creates a component or directive, it first calls its constructor to set up the initial state of the class and inject any required services. You should avoid heavy logic or operations that might cause side effects within the constructor.
import { Component } from '@angular/core';
import { MyService } from './my.service';
@Component({
selector: 'app-my-component',
templateUrl: './my-component.html',
styleUrls: ['./my-component.css']
})
export class MyComponent {
constructor(private myService: MyService) {
// This is primarily for dependency injection
console.log('Constructor called!');
// myService is available here
}
}
ngOnInit
ngOnInit is an Angular-specific lifecycle hook. It is called once, after the constructor, and after Angular has initialized all data-bound properties of a directive or component. This makes it a more suitable place for initialization logic that relies on those properties.
It's commonly used for fetching initial data, setting up subscriptions, or performing any complex initialization logic that might depend on input properties (@Input()) or other data bindings. It's also the first time you can reliably access these input properties.
import { Component, OnInit, Input } from '@angular/core';
import { MyService } from './my.service';
@Component({
selector: 'app-another-component',
templateUrl: './another-component.html',
styleUrls: ['./another-component.css']
})
export class AnotherComponent implements OnInit {
@Input() itemId: string;
data: any;
constructor(private myService: MyService) {
console.log('Constructor called. itemId:', this.itemId); // itemId might be undefined
}
ngOnInit(): void {
console.log('ngOnInit called. itemId:', this.itemId); // itemId is now available
if (this.itemId) {
this.myService.getData(this.itemId).subscribe(response => {
this.data = response;
});
}
}
}
Key Differences
- Invocation Timing:
constructoris called first (JS/TS feature);ngOnInitis called after (Angular lifecycle hook). - Purpose:
constructorfor dependency injection and basic setup;ngOnInitfor initialization logic after data-bound properties are set. - Access to @Input:
@Inputproperties are not available in theconstructor, but are guaranteed to be available inngOnInit. - Framework Specific:
constructoris a standard language feature;ngOnInitis specific to Angular. - Interface:
constructordoes not implement an interface;ngOnInitrequires implementing theOnInitinterface for type safety (though not strictly enforced by runtime).
When to use which
Use the constructor primarily for injecting services or other dependencies. Keep it lightweight and avoid complex logic.
Use ngOnInit for all other initialization logic. This includes fetching data, setting up subscriptions, or any operations that rely on Angular's data-bound properties (like @Input()).
How does Angular change detection work?
Angular's change detection mechanism is a core part of its reactivity, ensuring that the UI reflects the current state of the application's data. It automatically detects changes to data models and re-renders the necessary parts of the view, keeping the DOM in sync with your application state.
What is Change Detection?
Change detection is the process by which Angular monitors for changes in the application's data and updates the DOM to reflect those changes. Whenever data bound to a component's template changes, Angular needs to know about it to re-render the view efficiently.
How Does Angular Detect Changes?
Angular uses a unidirectional data flow from parent to child components. When an event or asynchronous operation occurs, Angular traverses the component tree from top to bottom, checking each component's bound properties for changes. It compares the current value with the previous value and, if a difference is found, updates the corresponding DOM element.
The Role of Zone.js
Zone.js is a key dependency of Angular that patches asynchronous browser APIs (like setTimeout, setInterval, XMLHttpRequest, event listeners). It creates a 'zone' where all asynchronous tasks are executed. When an asynchronous task completes, Zone.js notifies Angular that something potentially changed, triggering the change detection cycle.
Change Detection Strategies
Angular provides two strategies for change detection that control when a component's change detector is run:
- Default Strategy (CheckAlways): This is the default behavior. Angular checks all components from top to bottom on every change detection cycle, regardless of whether their inputs have changed. This is robust but can be less performant for large applications.
- OnPush Strategy (CheckOnce): With OnPush, a component's change detector is run only when its input properties change (by reference), when an event originates from the component or one of its children, or when explicitly triggered. This strategy can significantly improve performance by skipping checks for entire subtrees.
When is Change Detection Triggered?
The change detection cycle is typically triggered by Zone.js whenever one of the following asynchronous events occurs:
- Browser events (e.g., click, submit, input, keyup)
- HTTP requests via HttpClient
- Timers (e.g., setTimeout, setInterval)
- Promises and Observables (when they resolve or emit new values)
Optimizing Change Detection
For better performance, especially in larger applications, it's highly recommended to use the OnPush strategy where possible. When using OnPush, you might occasionally need to manually inform Angular about changes using ChangeDetectorRef methods like markForCheck(), detectChanges(), or detach() for fine-grained control over the change detection process.
What is Zone.js and why does Angular use it?
Zone.js is a powerful library that provides an execution context for asynchronous operations, allowing developers to observe and intercept the start and completion of these tasks. It effectively creates a 'zone' around asynchronous code, enabling a wide range of meta-programming techniques.
What is Zone.js?
At its core, Zone.js works by patching all known asynchronous APIs in the browser environment, such as setTimeout, setInterval, Promise, XMLHttpRequest, and DOM event listeners. When one of these patched APIs is called, Zone.js wraps the corresponding task within a conceptual 'zone'. This zone acts as an execution context that persists across asynchronous callbacks, allowing libraries to track when work begins and ends.
Essentially, Zone.js allows you to 'fork' the execution context and attach hooks to it. You can then run a piece of code within that forked context. Any asynchronous operations initiated within that context will inherit the zone and trigger its hooks upon completion. This makes it a foundational tool for frameworks needing to react to the completion of arbitrary asynchronous work.
Why Does Angular Use Zone.js?
Angular leverages Zone.js primarily for its change detection mechanism. Modern web applications are highly dynamic, with user interactions, HTTP requests, and timers constantly updating data. Angular needs a reliable way to know *when* the application's state might have changed so it can update the view accordingly.
Without Zone.js, developers would have to manually inform Angular after every asynchronous operation that the data model *might* have changed, by explicitly calling ChangeDetectorRef.detectChanges() or ApplicationRef.tick(). This would be boilerplate-heavy, error-prone, and significantly increase development complexity.
Zone.js solves this by providing the necessary hooks. When an asynchronous task (like a button click, an HTTP response, or a timer completing) finishes, Zone.js notifies Angular. Angular, through its NgZone service, then knows that an event has occurred that *might* affect the application state, and automatically triggers its change detection cycle. This ensures the UI is always in sync with the data model without manual intervention.
- Automatic Change Detection: Angular doesn't need to constantly poll for changes; it's notified when an asynchronous event completes, signaling a potential state change.
- Simpler Developer Experience: Developers don't have to manually trigger change detection, leading to cleaner and less error-prone code.
- Performance Optimization: By knowing precisely when to run change detection, Angular avoids unnecessary checks, improving application performance.
- Consistent Behavior: All asynchronous operations, regardless of their source (browser APIs, third-party libraries), are tracked consistently within Angular's execution context.
While powerful, Zone.js also introduces some overhead. For scenarios where maximum performance is critical and automatic change detection is not strictly needed for certain parts of the application, Angular provides mechanisms (like running code outside NgZone) to temporarily detach from Zone.js's tracking.