loading...

Aborting a fetch request

terabaud profile image Lea Rosema ・2 min read

When working with the vanilla JavaScript fetch() API, aborting a request is not too intuitive.

Why do we even need to abort a request?

One specific use case I needed an abortable request for was inside a React component. The component fetches some data at mount time and sets the fetched data inside the component's internal state.

Because the fetch is an asynchronous operation, the component could be unmounted before the fetch request is resolved. So, if you are working with a useEffect hook inside a React component, you have to provide a cleanup function that aborts the request.

How to abort a fetch request

Create an AbortController alongside with your fetch request and pass its signal property in the fetch options:

const { signal } = new AbortController();
const response = await fetch('https://yesno.wtf/api', {signal});
const data = await response.json();
// do something with data

In your cleanup function, you can then call the abort function via signal.abort();.

Wrapping it up

For my project, I wrapped it all up in a fetch wrapper class. In my project, I am using TypeScript and also took some decisions for my specific use case:

As only json data was needed, I hardcoded response.json() into it 💁‍♀️. Also I throw an exception if the response is anything else than 2xx okayish:

/**
 * Exceptions from the API
 */
export interface ApiException {
  status: number;
  details: any; 
}

/**
 * Request State
 */
export enum RequestState {
  IDLE = 'idle',
  ABORTED = 'aborted',
  PENDING = 'pending',
  READY = 'ready',
  ERROR = 'error'
}

/**
 * Ajax class
 * 
 * Wrapper class around the fetch API. 
 * It creates an AbortController alongside with the request.
 * Also, it keeps track of the request state and throws an ApiException on HTTP status code !== 2xx
 * 
 */
export class Ajax<T = any> {

  promise: Promise<Response> | null;
  abortController: AbortController | null;

  info: RequestInfo;
  init: RequestInit;

  state: RequestState;

  /**
   * Ajax constructor. Takes the same arguments as fetch()
   * @param info 
   * @param init 
   */
  constructor(info: RequestInfo, init?: RequestInit) {
    this.abortController = new AbortController();
    this.init = { ...(init || {}), signal: this.abortController.signal };
    this.info = info;
    this.state = RequestState.IDLE;
    this.promise = null;
  }

  /**
   * Send API request. 
   * 
   * @returns {any} json data (await (await fetch()).json())
   * @throws {ApiException} exception if http response status code is not 2xx
   * 
   */
  async send(): Promise<T> {
    this.state = RequestState.PENDING;
    try {
      this.promise = fetch(this.info, this.init);
      const response = await this.promise;
      const json = await response.json();
      if (! response.ok) {
        throw {status: response.status, details: json} as ApiException;
      }
      this.state  = RequestState.READY;
      return json;
    } catch (ex) {
      this.state = RequestState.ERROR;
      throw ex;
    } finally {
      this.abortController = null;
    }
  }

  /**
   * Cancel the request.
   */
  abort(): void {
    if (this.abortController) {
      this.state = RequestState.ABORTED;
      this.abortController.abort();
      this.abortController = null;
    }
  }
}

Usage:

const request = new Ajax('https://yesno.wtf/api');
const data = await request.send();

// abort it via:
request.abort();

Not sure if it really makes life easier, but it worked for me 💁‍♀️
I'd love to hear feedback about my solution and how to maybe simplify this. Also, I should have a look into all these http request libraries out there. If you have any recommendations, let me know in the comments.

Posted on by:

terabaud profile

Lea Rosema

@terabaud

Product Engineer by day, creative coder by night. Working at SinnerSchrader.

Discussion

markdown guide
 

Comparing between fetch and XMLHttpRequest for extra information:

Fetch

const controller = new AbortController;

fetch('/foo/bar', {
    signal: controller.signal
}).then(response => {
    if (response.ok) {
        alert(response.text());
    }
});

// Abort!
controller.abort();

XHR

const xhr = new XMLHttpRequest;

xhr.addEventListener('load', () => {
    if (xhr.status === 200) {
        xhr.responseType = 'text';
        alert(xhr.response);
    }
});

xhr.open('GET', '/foo/bar');
xhr.send();

// Abort!
xhr.abort();
 

I try abort in my project:

But failed. I use abort like this:

for(x of data){
 hello = $.get("/data")
}

$(".button").click(() => hello.abort())
 

As far as I am aware, this does not stop the request from being processed on your back end. So if you're changing filters and it automatically sends a request to the backend to query your database. You still end up with a lot of requests querying your database for no reason.

Just something to keep in the back of your mind!

 

Great article! Learnt something new today. A great use case for making API calls while changing filters on the frontend.

 

Finally, cancelling a network request is not one of the differences between RxJS observables and the Fetch API.
I actually learnt something here! Thanks!

 

I have learnt about this yesterday xD it was searching about giving a timeout to the fetch. I have not tried it yet, I started using axios instead of fetch. Thank you for the post!

 

Didn't know about the AbortController object. Thanks a lot for this article !