Unlock the Power of HTTP Request Cancellation in Angular
Tags: angular, rxjs, typescript
Published: 2023-05-23 07:47:44

---
title: Unlock the Power of HTTP Request Cancellation in Angular
published: true
description: For faster responses and smoother interactions
tags: #angular #rxjs #typescript
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9z18ltv642j7tow7qw2t.png
# Use a ratio of 100:42 for best results.
# published_at: 2023-05-23 06:59 +0000
canoncial_url: https://www.angularaddicts.com/p/unlock-the-power-of-http-request-cancellation
---

HTTP request cancellation in Angular means that you abort or cancel an ongoing HTTP request before it gets completed.

Here are a few situations in which request cancellation might be beneficial:

* In case the user makes **multiple similar requests in a short timeframe**, canceling unnecessary requests might help us optimize the performance. For example, if our app makes an API call on every key press in a search input field, that might cause unnecessary server load. We can optimize this by canceling previous requests if the user makes an additional keystroke. Our optimization efforts might lead to better response times and reduced server load. (You should also use the `debounceTime() `RxJs operator in this situation).

* Consider a scenario in which a user initiates an action, such as clicking a button or navigating away from a page, that triggers an HTTP request. If the user **suddenly changes their mind or performs another action**, canceling the previous request might prevent unnecessary network traffic and improve the user experience.

* Generally, when a **component level store is destroyed**, we should consider canceling the pending requests initiated by the store.

In this article, I demonstrate this by starting with a simple component that fetches data from the server, then I improve it in multiple steps. I explain you:

* How to avoid **race conditions**
* How to **automate** request cancellation when an additional request arrives
* How to **simplify** the cancellation logic with the `switchMap()` RxJs operator 

**The full source code is available here:**

[https://github.com/gergelyszerovay/angular-http-request-cancellation-rxjs](https://github.com/gergelyszerovay/angular-http-request-cancellation-rxjs)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3j1eixnn1qqgrnokfci9.png)

## Application architecture


### Backend

You can run the application's backend by `yarn run backend` or `npm run backend`. We use the following endpoints:

* `GET /data/2000` : after 2 seconds of delay, it returns the `{ message: 'You passed 2000 ms' }` JSON encoded object

* `GET /data/4000` : after 4 seconds of delay, it returns the `{ message: 'You passed 4000 ms' }` JSON encoded object

* `GET /error` : after 2 seconds of delay, it returns a 404 "not found" error

**Frontend**

I use Angular v16 with standalone components and Tailwind.css. You can run the frontend by `yarn run start` or `npm run start`.

The app bootstraps the `AppComponent` with the following child components:

* `FetchWithRxJs1Component`: Example 1: Basic RxJs (with user triggered request cancellation)

* `FetchWithRxJs2Component`: Example 2: RxJs (UI is disabled in loading state)

* `FetchWithRxJs3Component`: Example 3: RxJs (auto request cancellation)

* `FetchWithRxJs4Component`: Example 4: RxJs (auto request cancellation by switchMap())

* `FetchWithRxJs5Component`: Example 5: RxJs (user triggered + auto request cancellation by switchMap())

The application has a single UI component: `UiFetchComponent`, all `FetchWithRxJsComponent`s use this UI component to display the user interface.

## The UiFetchComponent component

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2zndntjmc53313l5qe9w.png)

The component has the following inputs:

* `title: string`: the caption text of the box

* `httpRequestState: HttpRequestState`: the request state

* `message: string`: the response text we got from the server

* `hasCancelButton: boolean`: false by default. If it's true, the box contains a 'Cancel all requests' button

* `isFetchDisabled: boolean`: false by default. If it's true, the "Fetch" buttons are disabled

Its outputs are the following:



* `onFetchData&lt;string>`: it emits when the user clicks on one of the fetch buttons. The emitted value is the path for the request: '/data/2000' or '/data/4000'

* `onCancel&lt;void>`: it emits when the user clicks on the 'Cancel all requests' button

The `httpRequestState` input contains the request state:


```typescript
export type HttpRequestState = DeepReadonly<
  'EMPTY' | 'FETCHING' | 'FETCHED' |
  { errorMessage: string }
  >;
```


Initially, its value is EMPTY. We change it to FETCHING right before we send a request to the server. When the server's response arrives, we set its value to FETCHED. If the server sends an error response or there is an error during the request, we set the request state to an `{ errorMessage: string }` object with the error message.


## Requesting data from the server by using the HttpClient.get() method

We use the `HttpClient.get()` method to fetch data from the server. The method returns an Observable. When we subscribe to this Observable, the `HttpClient` sends the request to the server. After the response is received from the server, this Observable will emit the server's response, then completes. If there is an error during the request, the Observable emits an error notification.

We can cancel a request by unsubscribing from the Observable returned by `HttpClient.get()`.

In the initial version of our data fetch component (Example 1: `FetchWithRxJs1Component`), we initiate a request each time the user clicks on one of the "Fetch" buttons. We store the request's Subscriptions in the `subscriptions` array.

If the user clicks on the "Cancel all requests" button, we unsubscribe from all the requests stored in the `subscriptions` array (`cancelAllRequests()` function):


```typescript
// fetch-with-rxjs1.component.ts
@Component({
  selector: 'app-fetch-with-rxjs1',
  // ...
  template: `
<app-ui-fetch-component style="display: block"
  [httpRequestState]="httpRequestState$ | async"
  [message]="message$ | async"
  [hasCancelButton]="true"
  title="Example 1: Basic RxJs"
  (onFetchData)="fetchData($event)"
  (onCancel)="cancelAllRequests()"
>
</app-ui-fetch-component>
   `
})
export class FetchWithRxJs1Component {
  private http = inject(HttpClient);

  protected httpRequestState$ = new BehaviorSubject<HttpRequestState>('EMPTY');
  protected message$ = new BehaviorSubject<string | null>(null);

  protected subscriptions: Subscription[] = [];

  cancelAllRequests() {
    if (!this.subscriptions) {
      return;
    }
    this.subscriptions.forEach(s => s.unsubscribe());
    this.subscriptions = [];
    this.httpRequestState$.next({ errorMessage: 'All requests were canceled' });
  }

  fetchData(path: string) {
    const url = `http://localhost:3000/${path}`;
    console.log('http.get', url);
    this.httpRequestState$.next('FETCHING');
    const subscription = this.http.get<MessageResponseType>(url)
      .pipe(
        catchError((errorResponse: HttpErrorResponse) => {
          this.httpRequestState$.next({ errorMessage: 'Request error' });
          return EMPTY;
        }),
      ).subscribe((response) => {
        this.message$.next(response.message);
        this.httpRequestState$.next('FETCHED');
      });
    this.subscriptions.push(subscription);
  }
}
```


For example, if the user clicks twice on the "Fetch from /data/4000 (slower)" button within a short timeframe, then clicks on the "Cancel all requests" button, all the requests get canceled:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p0m2pbw7vi4giedpb8um.png)

## Race conditions

However, user triggered cancellation doesn't protect us from the effects of race conditions and unnecessary requests. For example, if the user clicks on the "Fetch from /data/4000 (slower)" button, then quickly after that on the "Fetch from /data/2000 (fast)" button, we get the server responses in reverse order: first for the `/data/2000` request, then for the `/data/4000` request, so as a result the "You passed 4000 ms" message is displayed, but we might expect to see the "You passed 2000 ms" message, as the last thing the user interacted with was the "Fetch from /data/2000 (fast)" button. You can see the exact timing of the requests in the "Network" tab of the DevTools:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wkxqa5h0t2k3el059t0g.png)

We can prevent these race conditions by simply disabling the buttons until our request is pending. We can disable the buttons by using the `isFetchDisabled` input of our UI component. So this is the improved source code (Example 2: `FetchWithRxJs2Component`):


```typescript
// fetch-with-rxjs2.component.ts
@Component({
  // ...
  template: `
<app-ui-fetch-component
  [httpRequestState]="httpRequestState$ | async"
  [message]="message$ | async"
  [hasCancelButton]="true"
  [isFetchDisabled]="(httpRequestState$ | async) === 'FETCHING'" 👈
  title="RxJs (UI is disabled in loading state)"
  (onFetchData)="fetchData($event)"
  (onCancel)="cancelAllRequests()"
>
</app-ui-fetch-component>
  `
})
```


After the user clicks on a "Fetch" button, we disable the buttons until we get the response from the server:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iu4vpqdcgeuq3kyf3e56.png)

## Automatic request cancellation

When the user for example submits a form, disabling the UI controls might work, but this approach is not viable in many other cases. In our previous example, we had an app that makes an API call on every key press in a search input field. If we disabled the input field after every keystroke, it would lose its focus and the control would become unusable.

To apply a different approach, we can automatically cancel the pending requests if the user clicks on one of the "Fetch" buttons. I refactor the component (Example 3: `FetchWithRxJs3Component`):



* I add a new parameter to the `cancelAllRequests()` function to make the state update optional

* I call the `cancelAllRequests(false)` function from the `fetchData()` function to cancel all previous requests


```typescript
// fetch-with-rxjs3.component.ts
export class FetchWithRxJs3Component {
  // ...
  cancelAllRequests(updateState = true) {
    if (!this.subscriptions) {
      return;
    }
    this.subscriptions.forEach(s => s.unsubscribe());
    this.subscriptions = [];
    if (updateState) {
      this.httpRequestState$.next({ errorMessage: 'All requests were canceled' });
    }
  }

  fetchData(path: string) {
    const url = `http://localhost:3000/${path}`;
    console.log('http.get', url);
    this.cancelAllRequests(false);
    this.httpRequestState$.next('FETCHING');
    const subscription = this.http.get<MessageResponseType>(url)
      .pipe(
        catchError((errorResponse: HttpErrorResponse) => {
          this.httpRequestState$.next({ errorMessage: 'Request error' });
          return EMPTY;
        }),
      ).subscribe((response) => {
        this.message$.next(response.message);
        this.httpRequestState$.next('FETCHED');
      });
    this.subscriptions.push(subscription);
  }
}
```


Now if the user clicks on the "Fetch" buttons multiple times, we can see that we canceled all the previous requests, only the last request gets a response:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t6n1anoov68z25yeigsp.png)

We can simplify this code by using the `switchMap()` RxJs operator. I make the following changes in the code (Example 4: `FetchWithRxJs4Component`):



* I introduce the `triggerFetch$` `BehaviorSubject`. It triggers the HTTP request. I move the request triggering logic into the `constructor()`.

* The `fetchData()` function is now just a single line, it calls `triggerFetch$.next(...)` to trigger the HTTP request

* We don't need the `cancelAllRequests()` function anymore, as the `switchMap()` operator automatically cancels the previous request


```typescript
// fetch-with-rxjs4.component.ts
export class FetchWithRxJs4Component {
  // ...
 protected triggerFetch$ = new BehaviorSubject<string>('');
  cancelAllRequests(updateState = true) {
    if (!this.subscriptions) {
      return;
    }
    this.subscriptions.forEach(s => s.unsubscribe());
    this.subscriptions = [];
    if (updateState) {
      this.httpRequestState$.next({ errorMessage: 'All requests were canceled' });
    }
  }

  constructor() {
    this.triggerFetch$.pipe(
      skip(1), // we doesn't want an initial fetch on component creation
      takeUntilDestroyed(),
      tap(() => {
        this.cancelAllRequests(false);
        this.httpRequestState$.next('FETCHING')
      }),
      switchMap((path) => {
        const url = `http://localhost:3000/${path}`;
        console.log('http.get', url);
        return this.http.get<MessageResponseType>(url)
          .pipe(
            catchError((errorResponse: HttpErrorResponse) => {
              this.httpRequestState$.next({ errorMessage: 'Request error' });
              return EMPTY;
            })
          );
        })).subscribe((response) => {
          this.message$.next(response.message);
          this.httpRequestState$.next('FETCHED');
        });
  }

  fetchData(path: string) {
    this.triggerFetch$.next(path);
  }
}
```



## How does the switchMap() operator work?

When the user clicks on one of the "Fetch" buttons, we emit a value through the `triggerFetch$` `BehaviorSubject`. The `switchMap()` gets this value through the `pipe()`, and initiates a new HTTP request by subscribing to the Observable returned by `HttpClient.get()`.

Now, if the user clicks on one of the "Fetch" buttons again, `switchMap()` gets a new value from `triggerFetch$`, so it automatically unsubscribes from the previous Observable and starts a new HTTP request by subscribing to the Observable returned by `HttpClient.get()`.


## What if we would like to support user canceling, too?

To add user canceling, I change (Example 5: `FetchWithRxJs5Component`):



* The data type of `triggerFetch$` to `{ path?: string, cancel?: boolean }`. This way we can both start a new request or cancel the previous one by setting the cancel property to true

* The `fetchData()` function emits the `{ path }` value through `triggerFetch$`

* The cancelAllRequests() function emits the `{ cancel: true }` value through `triggerFetch$`

* Inside the `switchMap`'s projector function, if the `cancel` property of the input value is true, we return an empty Observable that cancels the previous request

```ts
// fetch-with-rxjs5.component.ts
export class FetchWithRxJs5Component {
  // ...
 protected triggerFetch$ = new BehaviorSubject<{ path?: string, cancel?: boolean }>(
    { path: '' });

  constructor() {
    this.triggerFetch$.pipe(
      skip(1), // we doesn't want an initial fetch on component creation
      takeUntilDestroyed(),
      tap(() => this.httpRequestState$.next('FETCHING')),
      switchMap(({ path, cancel }) => {
        if (cancel) { // 👈
          return of();
        }
        const url = `http://localhost:3000/${path}`;
        console.log('http.get', url);
        return this.http.get<MessageResponseType>(url)
          .pipe(
            tap((response) => {
              this.message$.next(response.message);
              this.httpRequestState$.next('FETCHED');
            }),
            catchError((errorResponse: HttpErrorResponse) => {
              this.httpRequestState$.next({ errorMessage: 'Request error' });
              return EMPTY;
            })
          );
        })).subscribe();
  }

  cancelRequests() {
    this.triggerFetch$.next({ cancel: true });
  }

  fetchData(path: string) {
    this.triggerFetch$.next({ path });
  }
}
```


In the second part of this article series, I’m going to explain how we can handle HTTP request cancellation inside an NgRx `ComponetStore` or `SignalStore`.

---

## 👨‍💻About the author

My name is [Gergely Szerovay](https://dev.to/gergelyszerovay), I work as a frontend development chapter lead. Teaching (and learning) Angular is one of my passions. I consume content related to Angular on a daily basis — articles, podcasts, conference talks, you name it.

I created the Angular Addict Newsletter so that I can send you the best resources I come across each month. Whether you are a seasoned Angular Addict or a beginner, I got you covered.

Next to the newsletter, I also have a publication called — you guessed it — [Angular Addicts](https://angularaddicts.com/). It is a collection of the resources I find most informative and interesting. Let me know if you would like to be included as a writer.

Let’s learn Angular together! [Subscribe here](https://newsletter.angularaddicts.com/) 🔥

Follow me on [Medium](https://medium.com/@GergelySzerovay), [Twitter](https://twitter.com/GergelySzerovay) or [LinkedIn](https://www.linkedin.com/in/gergelyszerovay/) to learn more about Angular!