4

I am implementing abortable fetch calls.

There are basically two reasons for aborting the fetch on my page:

  • the user decides he/she does not want to wait for the AJAX data anymore and clicks a button; in this case the UI shows a message "call /whatever interrupted"
  • the user has moved to another part of the page and the data being fetched are no longer needed; in this case I don't want the UI to show anything, as it'd just confuse the user

In order to discriminate the two cases I was planning to use the reason parameter of the AbortController.abort method, but the .catch clause in my fetch call always receives a DOMException('The user aborted a request', ABORT_ERROR).

I have tried to provide a different DOMException as reason for the abort in case 2, but the difference is lost.

Has anyone found how to send information to the fetch .catch clause with regards to the reason to abort?

Marco Faustinelli
  • 3,734
  • 5
  • 30
  • 49

1 Answers1

5

In the example below, I demonstrate how to determine the reason for an abortion of a fetch request. I provide inline comments for explanation. Feel free to comment if anything is unclear.

Re-run the code snippet to see a (potentially different) random result

'use strict';

function delay (ms, value) {
  return new Promise(res => setTimeout(() => res(value), ms));
}

function getRandomInt (min = 0, max = 1) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

// Forward the AbortSignal to fetch:
// https://docs.github.com/en/rest/repos/repos#list-public-repositories
function fetchPublicGHRepos (signal) {
  const headers = new Headers([['accept', 'application/vnd.github+json']]);
  return fetch('https://api.github.com/repositories', {headers, signal});
}

function example () {
  const ac = new AbortController();
  const {signal} = ac;

  const abortWithReason = (reason) => delay(getRandomInt(1, 5))
    .then(() => {
      console.log(`Aborting ${signal.aborted ? 'again ' : ''}(reason: ${reason})`);
      ac.abort(reason);
    });

  // Unless GitHub invests HEAVILY into our internet infrastructure,
  // one of these promises will resolve before the fetch request
  abortWithReason('Reason A');
  abortWithReason('Reason B');

  fetchPublicGHRepos(signal)
    .then(res => console.log(`Fetch succeeded with status: ${res.status}`))
    .catch(ex => {
      // This is how you can determine if the exception was due to abortion
      if (signal.aborted) {
        // This is set by the promise which resolved first
        // and caused the fetch to abort
        const {reason} = signal;
        // Use it to guide your logic...
        console.log(`Fetch aborted with reason: ${reason}`);
      }
      else console.log(`Fetch failed with exception: ${ex}`);
    });

  delay(10).then(() => console.log(`Signal reason: ${signal.reason}`));
}

example();
jsejcksn
  • 27,667
  • 4
  • 38
  • 62
  • Hmmm... this is not what I hoped for, because I already do lots of things with the fetch().catch clause and I planned to use it also in this case. But I guess this is the way to go. Thank you! – Marco Faustinelli Jul 20 '22 at 11:30
  • @MarcoFaustinelli No problem. Although, I'm not sure what you mean by "_this is not what I hoped for, because I already do lots of things with the fetch().catch clause and I planned to use it also in this case._". Want to clarify? – jsejcksn Jul 21 '22 at 00:14
  • In my stack, which handles a very complex range of intrastructure security policies, the fetch invocation is wrapped inside a custom Promise. Depending on the error arriving in the fetch().catch clause, I reject the custom Promise in different manners. There are even errors which causes the custom Promise to resolve. In my view an aborted fetch is not an error, so I was planning to resolve it into a 299. I don't recall where I read about 299's, but I like the concept. I still have to think how to integrate your solution in my stack, but I will manage :-) – Marco Faustinelli Jul 21 '22 at 14:33
  • 1
    @MarcoFaustinelli Thanks for explaining. I think most of us start out thinking “cancellation isn’t an error case!” (but thinking critically about the alternatives might lead to another conclusion). You might be interested in reading some history on this topic in the [False starts in TC39](https://developer.chrome.com/blog/abortable-fetch/#false-starts-in-tc39) section of the _Abortable Fetch_ article by Jake Archibald. – jsejcksn Jul 21 '22 at 18:25
  • Yes, I used to be familiar with the cancelable promises effort. I never liked them from a functional-programming point of view (promises are the dual of optionals, aka maybes, so a third possible outcome is _ugly_) and I am glad the final choice was otherwise. I'd like to know which alternatives you encountered that through "critical thinking" led you to change view about the nature of cancellations. – Marco Faustinelli Jul 22 '22 at 14:39
  • @MarcoFaustinelli Aside: The way you quoted my phrase makes me think it didn't land well with you, and if you considered it as directed personally, please know that I didn't mean it that way. Socratic response: If a `fetch` call returns a promise with only [a single type](https://developer.mozilla.org/en-US/docs/Web/API/fetch#return_value) (`Promise`), then what could possibly be returned if you cancel it before completion? The only option in that state is to take the other path (reject). – jsejcksn Jul 22 '22 at 21:46
  • More: Generally, rejection cases aren't necessarily indicative of errors, and neither are all exceptions — whose values aren't necessarily [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error)s (although I'd argue that they probably should be for easier reasoning) — rejections and exceptions are simply control flow mechanisms with established conventions (by both the language itself and the community). – jsejcksn Jul 22 '22 at 21:52
  • Contrived example to illustrate: https://tsplay.dev/N71aEW – jsejcksn Jul 22 '22 at 22:03
  • No sweat :-) I used the doublequotes to pinpoint that you promised a well thought explanation, and that I was expecting nothing less. In my case, the reasoning is less "critical": errors scare users and make them thing they did something wrong; moreover, in many cases I know what else to do. So, a resolve-2XX does not flare up alerts in the UI, while I sort out things in the next .then(). – Marco Faustinelli Jul 26 '22 at 10:44