How to Cancel Previous HTTP Requests Using RxJS in Angular

Learn how to efficiently cancel previous HTTP requests in Angular using RxJS operators like switchMap, takeUntil, and Subject. Prevent race conditions and improve app performance with these proven techniques.

RxJs

When building Angular applications with RxJS, managing HTTP requests efficiently is crucial for performance and user experience. One common challenge developers face is handling multiple concurrent HTTP requests that can lead to race conditions, unnecessary server load, and poor application performance.

In this comprehensive guide, we’ll explore different approaches to cancel previous HTTP requests using RxJS operators and patterns. We’ll cover:

Understanding the Problem of Multiple HTTP Requests

In real-world applications, especially those with search functionality, users often trigger multiple HTTP requests rapidly. Consider a search input where users type quickly:

// Problematic approach - can cause race conditions
search(term: string) {
  this.http.get(`/api/search?q=${term}`)
    .subscribe(results => {
      this.searchResults = results;
    });
}

If a user types “angular” quickly, this could trigger 7 separate HTTP requests:

The responses might arrive out of order, causing the UI to show incorrect results. We need to cancel previous requests when new ones are made.

The switchMap operator is the most elegant solution for canceling previous HTTP requests. It automatically cancels the previous observable when a new one is emitted.

import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Subject, Observable } from 'rxjs';
import { switchMap, debounceTime, distinctUntilChanged } from 'rxjs/operators';

@Component({
  selector: 'app-search',
  template: `
    <input 
      type="text" 
      [formControl]="searchControl" 
      placeholder="Search..."
    />
    <div *ngFor="let result of searchResults">
      {{ result.name }}
    </div>
  `
})
export class SearchComponent implements OnInit {
  searchControl = new FormControl('');
  searchResults: any[] = [];
  private searchSubject = new Subject<string>();

  constructor(private http: HttpClient) {}

  ngOnInit() {
    // Set up the search stream
    this.searchSubject.pipe(
      debounceTime(300), // Wait 300ms after user stops typing
      distinctUntilChanged(), // Only emit if value changed
      switchMap(term => this.http.get(`/api/search?q=${term}`))
    ).subscribe(results => {
      this.searchResults = results;
    });

    // Listen to input changes
    this.searchControl.valueChanges.subscribe(term => {
      this.searchSubject.next(term);
    });
  }
}

Key benefits of switchMap:

Method 2: Using takeUntil Operator with Subject

For more control over when to cancel requests, you can use the takeUntil operator with a Subject:

import { Component, OnDestroy } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Component({
  selector: 'app-search',
  template: `
    <input 
      type="text" 
      (input)="onSearch($event)" 
      placeholder="Search..."
    />
    <button (click)="cancelSearch()">Cancel</button>
  `
})
export class SearchComponent implements OnDestroy {
  private cancelSubject = new Subject<void>();
  private destroy$ = new Subject<void>();

  constructor(private http: HttpClient) {}

  onSearch(event: any) {
    const term = event.target.value;
    
    // Cancel previous request
    this.cancelSubject.next();
    
    // Make new request
    this.http.get(`/api/search?q=${term}`)
      .pipe(takeUntil(this.cancelSubject))
      .subscribe({
        next: (results) => {
          console.log('Search results:', results);
        },
        error: (error) => {
          console.error('Search error:', error);
        }
      });
  }

  cancelSearch() {
    this.cancelSubject.next();
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Method 3: Advanced Search with Multiple Operators

For production applications, you might want to combine multiple operators for optimal performance:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormControl } from '@angular/forms';
import { Subject, Observable, of } from 'rxjs';
import { 
  switchMap, 
  debounceTime, 
  distinctUntilChanged, 
  catchError, 
  startWith,
  takeUntil 
} from 'rxjs/operators';

@Component({
  selector: 'app-advanced-search',
  template: `
    <div class="search-container">
      <input 
        type="text" 
        [formControl]="searchControl" 
        placeholder="Search products..."
        class="search-input"
      />
      <div *ngIf="loading" class="loading">Searching...</div>
      <div *ngIf="error" class="error">{{ error }}</div>
      <div *ngFor="let result of searchResults" class="result-item">
        {{ result.name }}
      </div>
    </div>
  `
})
export class AdvancedSearchComponent implements OnInit, OnDestroy {
  searchControl = new FormControl('');
  searchResults: any[] = [];
  loading = false;
  error: string | null = null;
  
  private destroy$ = new Subject<void>();

  constructor(private http: HttpClient) {}

  ngOnInit() {
    this.searchControl.valueChanges.pipe(
      startWith(''), // Emit initial value
      debounceTime(400), // Wait 400ms after user stops typing
      distinctUntilChanged(), // Only emit if value actually changed
      switchMap(term => {
        if (!term.trim()) {
          return of([]); // Return empty array for empty search
        }
        
        this.loading = true;
        this.error = null;
        
        return this.http.get(`/api/search?q=${encodeURIComponent(term)}`).pipe(
          catchError(error => {
            this.error = 'Search failed. Please try again.';
            return of([]);
          })
        );
      }),
      takeUntil(this.destroy$)
    ).subscribe(results => {
      this.searchResults = results;
      this.loading = false;
    });
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Method 4: Creating Reusable Operators

For applications that need request cancellation in multiple places, you can create custom operators:

import { OperatorFunction, Observable, Subject } from 'rxjs';
import { takeUntil, switchMap } from 'rxjs/operators';

// Custom operator for canceling previous requests
export function cancelPrevious<T, R>(
  project: (value: T) => Observable<R>
): OperatorFunction<T, R> {
  return (source: Observable<T>) => {
    const cancelSubject = new Subject<void>();
    
    return source.pipe(
      switchMap(value => {
        cancelSubject.next(); // Cancel previous request
        return project(value).pipe(takeUntil(cancelSubject));
      })
    );
  };
}

// Usage example
@Component({
  selector: 'app-reusable-search'
})
export class ReusableSearchComponent {
  searchControl = new FormControl('');

  constructor(private http: HttpClient) {
    this.searchControl.valueChanges.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      cancelPrevious(term => this.http.get(`/api/search?q=${term}`))
    ).subscribe(results => {
      console.log('Search results:', results);
    });
  }
}

Best Practices and Tips

  1. Use switchMap for search scenarios - It’s the most appropriate operator for canceling previous requests
  2. Always implement OnDestroy - Clean up subscriptions to prevent memory leaks
  3. Add error handling - Use catchError to handle failed requests gracefully
  4. Use debounceTime - Prevent excessive API calls during rapid user input
  5. Use distinctUntilChanged - Avoid duplicate requests for the same search term
  6. Consider loading states - Show loading indicators during requests

Performance Considerations

Conclusion

Canceling previous HTTP requests in Angular with RxJS is essential for building performant applications. The switchMap operator is the most elegant solution for most use cases, while takeUntil provides more control when needed. By combining these operators with debounceTime and distinctUntilChanged, you can create robust search functionality that handles user input efficiently.

Remember to always clean up subscriptions and implement proper error handling to ensure your application remains stable and responsive.

Key takeaways:

Looking for a Full-Stack JavaScript Developer?

If you're hiring for a full-stack JavaScript developer position and found this article helpful, I'd love to contribute to your team! I specialize in modern JavaScript technologies including Node.js, NestJS, Angular, PostgreSQL, Docker, and Kubernetes, with extensive experience in RxJS, TypeScript, and building scalable web applications.

Let's connect:

  • 📧 Email: jobayer6735@gmail.com
  • Schedule a meeting
  • 💼 View my portfolio and other technical articles on this site

I'm passionate about writing clean, maintainable code and sharing knowledge with the developer community. If your company values technical excellence and continuous learning, I'd be excited to discuss how I can help your team succeed.

Jobayer

© 2025 Jobayer Ahmed. All rights are reserved.