DEV Community

Lea Rosema (she/her)
Lea Rosema (she/her)

Posted on

Aborting a fetch request

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
Enter fullscreen mode Exit fullscreen mode

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;
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Usage:

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

// abort it via:
request.abort();
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (6)

Collapse
 
taufik_nurrohman profile image
Taufik Nurrohman • Edited

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();
Collapse
 
mzaini30 profile image
Zen

I try abort in my project:

But failed. I use abort like this:

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

$(".button").click(() => hello.abort())
Collapse
 
ryands17 profile image
Ryan Dsouza

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

Collapse
 
orimdominic profile image
Orim Dominic Adah

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

Collapse
 
diek profile image
diek

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!

Collapse
 
alexandrecoin profile image
Alexandre C.

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