Aborting a fetch request
Tags: fetch, vanillajs, javascript
Published: 2020-02-16 11:51:36

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:

```js
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:

```ts
/**
 * 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:
```ts
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.