DEV Community

Cover image for Fetch-22
Mads Stoumann
Mads Stoumann

Posted on

Fetch-22

Browsers have supported fetch() for years now (except Internet Explorer), but I still see many developers use classic XHR-based "ajax".

Why is that? I think — partly — it's because fetch() is missing timeout and an easier way of handling errors. Yet, developers do want to use it because of its simplicity.
In other words: it's a fetch-22 situation (bad pun intended!)

I've written a small module, fetch22(), which extends the fetch() init-object with:

  • callback(true|false) : function to call when fetch starts(true) and ends(false)
  • errorHandler(error) : custom function to run if an error occurs
  • errorList : Array of status-codes to manually trigger errors
  • parser(response) : custom function to parse the response, default will use .json() or .text(), depending on response content-type
  • parserArgs : Array of extra arguments to send to the custom parser after response
  • timeout : period in milliseconds to allow before a Timeout Error. Default is 9999ms

The module itself can be grabbed here, and a Pen with examples can be seen here (but go to Codepen and see it in full-screen):

Here are some of the examples from the demo:

Custom callback

The custom callback is called twice, once when the fetch initiates, and then when the fetch is complete done:

function startStop(bool) {
  if (bool) {
    console.log('START'}
  else {
    console.log('STOP');
  }
}
Enter fullscreen mode Exit fullscreen mode

Custom ErrorHandler

Receives the error-object as it's only param:

function handleError(error) {
  console.error(error.name, error.message);
}
Enter fullscreen mode Exit fullscreen mode

Custom parser

The default parser returns json or text, depending on content-type. An example of a custom parser, finding a <symbol> by id in an svg:

async function getSymbolFromSVG(response, id) {
  const text = await response.text();
  const parser = new DOMParser();
  const doc = parser.parseFromString(text, "text/xml");
  const svg = doc.getElementById(id);
  return svg.outerHTML.toString();
}
Enter fullscreen mode Exit fullscreen mode

Hope it will be as useful for you, as it has been for me.

Thanks for reading, and happy coding!

Top comments (6)

Collapse
 
aminnairi profile image
Amin

Hi there, very interesting topic, thanks for this library!

You can use a combination of setTimeout, AbortController and the Fetch API to achieve a similar result, while still leveraging the power of promises.

const createSignalWithTimeout = (seconds) => {
  const milliseconds = seconds * 1000;
  const abortController = new AbortController();
  const signal = abortController.signal;

  let timeout = null;

  timeout = window.setTimeout(abortController.abort, milliseconds);

  const cancelSignal = () => {
    window.clearTimeout(timeout);
  };

  return { cancelSignal, signal };
};

const { signal, cancelSignal } = createSignalWithTimeout(1);

fetch("https://jsonplaceholder.typicode.com/users", { signal }).then((response) => {
  if (response.ok) {
    cancelSignal();
    return response.json();
  }

  throw new Error("Bad response from the server");
}).then((users) => {
  console.log(users);
}).catch(({ message }) => {
  console.error(message);
});
Enter fullscreen mode Exit fullscreen mode

This is a very great use-case for using this obscure AbortController. Although you don't really manage what error message is thrown, you can still detect the instanceof the error which will be AbortError in that case. See MDN.

Collapse
 
madsstoumann profile image
Mads Stoumann • Edited

It's exactly what I'm using, if you look at the code - it just adds the AbortError to the same errorHandler.
The result is a promise ;-)

  /* Handle timeout / AbortController */
  if ('AbortController' in window) {
    const controller = new AbortController();
    const signal = controller.signal;
    settings.signal = signal;
    setTimeout(() => {return controller.abort()}, timeout);
  }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
aminnairi profile image
Amin • Edited

You are correct, I didn't look at your code, very well done then!

I'll let this code snippet for people wanting an alternative without the start/stop callback or the error list.

Thread Thread
 
madsstoumann profile image
Mads Stoumann

👍

Collapse
 
alfredosalzillo profile image
Alfredo Salzillo

Can you show the benefit of the callback and errorHandler and parser over the then/catch of promise ?

Collapse
 
madsstoumann profile image
Mads Stoumann • Edited

I think it's just an easier syntax, parsing one init-object with these extra options. The result itself is a Promise, the start/stop callback is called on init, and in the .finally()-block. The errorHandler takes care of "AbortErrors" (timeout) as well:

catch(error => {
      if (error.name === 'AbortError') {
        errorHandler({
          name: 'FetchError',
          message: `Timeout after ${timeout} milliseconds.`,
          response: '',
          status: 524
        });
      } else {
        errorHandler(error);
      }
Enter fullscreen mode Exit fullscreen mode