Observables vs Promises in Angular

Question 1: What are the key differences between Observables and Promises?

Answer: Key differences include:

  1. Execution

    • Promises: Execute immediately when created
    • Observables: Lazy, only execute when subscribed to
  2. Values

    • Promises: Resolve once with a single value
    • Observables: Can emit multiple values over time
  3. Cancellation

    • Promises: Cannot be cancelled
    • Observables: Can be cancelled by unsubscribing
  4. Operations

    • Promises: Limited operations (then, catch, finally)
    • Observables: Rich set of operators for data transformation

Question 2: How do you use Observables and Promises in Angular applications?

Answer: Here’s a comprehensive comparison:

// Service demonstrating both approaches
@Injectable({
  providedIn: 'root'
})
export class DataService {
  private http = inject(HttpClient);
  private apiUrl = inject(API_URL);

  // Promise-based approach
  async getUserPromise(id: string): Promise<User> {
    try {
      const response = await fetch(`${this.apiUrl}/users/${id}`);
      if (!response.ok) throw new Error('User not found');
      return response.json();
    } catch (error) {
      console.error('Error fetching user:', error);
      throw error;
    }
  }

  // Observable-based approach
  getUserObservable(id: string): Observable<User> {
    return this.http.get<User>(`${this.apiUrl}/users/${id}`).pipe(
      tap(user => console.log('User fetched:', user)),
      catchError(error => {
        console.error('Error fetching user:', error);
        return throwError(() => error);
      })
    );
  }

  // Real-time updates (only possible with Observables)
  getUserUpdates(id: string): Observable<User> {
    return new Observable<User>(observer => {
      // WebSocket connection
      const ws = new WebSocket(`${this.wsUrl}/users/${id}`);
      
      ws.onmessage = event => {
        observer.next(JSON.parse(event.data));
      };
      
      ws.onerror = error => {
        observer.error(error);
      };
      
      // Cleanup on unsubscribe
      return () => ws.close();
    });
  }
}

// Component using both approaches
@Component({
  selector: 'app-user',
  template: `
    <!-- Promise approach -->
    @if (userFromPromise) {
      <div>{{ userFromPromise.name }}</div>
    }

    <!-- Observable approach -->
    @if (user$ | async; as user) {
      <div>{{ user.name }}</div>
    }

    <!-- Real-time updates -->
    @if (userUpdates$ | async; as user) {
      <div>Last updated: {{ user.lastUpdate | date:'medium' }}</div>
    }
  `
})
export class UserComponent {
  private dataService = inject(DataService);
  
  // Promise approach
  userFromPromise?: User;
  
  // Observable approach
  user$ = this.dataService.getUserObservable('123');
  
  // Real-time updates
  userUpdates$ = this.dataService.getUserUpdates('123').pipe(
    shareReplay(1)  // Share the connection
  );
  
  async ngOnInit() {
    // Promise usage
    try {
      this.userFromPromise = await this.dataService.getUserPromise('123');
    } catch (error) {
      console.error('Failed to load user:', error);
    }
  }
}

Question 3: How do you convert between Promises and Observables?

Answer: Here are conversion patterns:

@Injectable({
  providedIn: 'root'
})
export class ConversionService {
  // Convert Promise to Observable
  promiseToObservable<T>(promise: Promise<T>): Observable<T> {
    return from(promise);
  }
  
  // Convert Observable to Promise
  observableToPromise<T>(observable: Observable<T>): Promise<T> {
    return firstValueFrom(observable);
  }
  
  // Example usage
  async getData() {
    // Promise to Observable
    const promise = fetch('/api/data');
    const observable$ = from(promise);
    
    // Observable to Promise
    const httpObservable$ = this.http.get('/api/data');
    const result = await firstValueFrom(httpObservable$);
    
    // Handle multiple emissions
    const values = await lastValueFrom(observable$);
  }
}

Interview Tips 💡

  1. When to Use Each

    // Use Promises when:
    async function singleOperation() {
      const result = await somePromise;
      return process(result);
    }
    
    // Use Observables when:
    const stream$ = someObservable$.pipe(
      // Multiple values over time
      // Cancellation needed
      // Complex transformations required
    );
  2. Error Handling Patterns

    // Promise error handling
    async function handlePromise() {
      try {
        const result = await promise;
        return result;
      } catch (error) {
        handleError(error);
      }
    }
    
    // Observable error handling
    observable$.pipe(
      catchError(error => {
        handleError(error);
        return EMPTY;
      }),
      retry(3)
    );
  3. Memory Management

    export class Component implements OnDestroy {
      private destroy$ = new Subject<void>();
      
      ngOnInit() {
        // Auto-cleanup subscriptions
        this.data$.pipe(
          takeUntil(this.destroy$)
        ).subscribe();
      }
      
      ngOnDestroy() {
        this.destroy$.next();
        this.destroy$.complete();
      }
    }
  4. Performance Considerations

    // Share expensive Observable operations
    const sharedData$ = this.getData().pipe(
      shareReplay(1)
    );
    
    // Cache Promise results
    const cachedPromise = (async () => {
      const result = await expensiveOperation();
      return result;
    })();
  5. Testing Strategies

    describe('DataComponent', () => {
      // Testing Promise
      it('should load data from promise', async () => {
        const result = await component.loadDataPromise();
        expect(result).toBeDefined();
      });
      
      // Testing Observable
      it('should stream data', (done) => {
        component.data$.subscribe(data => {
          expect(data).toBeDefined();
          done();
        });
      });
    });
  6. Common Pitfalls

    // Memory leak - No unsubscribe
    ngOnInit() {
      this.data$.subscribe(); // Bad
      
      // Good - with cleanup
      this.data$.pipe(
        takeUntil(this.destroy$)
      ).subscribe();
    }
    
    // Promise not handled
    somePromise(); // Bad
    
    // Good - handle or await
    await somePromise();
    
    // Multiple subscriptions
    const data$ = this.getData().pipe(
      share() // Share subscription
    );

Remember: In interviews, focus on:

  • Understanding fundamental differences
  • Knowing when to use each
  • Memory management
  • Error handling
  • Performance implications
  • Testing approaches
  • Common pitfalls and solutions
  • Real-world scenarios where one is better than the other

Test Your Knowledge

Take a quick quiz to test your understanding of this topic.

Test Your Angular Knowledge

Ready to put your skills to the test? Take our interactive Angular quiz and get instant feedback on your answers.