Skip to content

Instantly share code, notes, and snippets.

@NekR
Created March 25, 2015 23:17
Show Gist options
  • Save NekR/065054dac4fb6d0b5338 to your computer and use it in GitHub Desktop.
Save NekR/065054dac4fb6d0b5338 to your computer and use it in GitHub Desktop.
Why so fetch?

Why so fetch?

This is answer to this Jake's post http://jakearchibald.com/2015/thats-so-fetch/

First, I would begin with commenting this shining phrase:

Bit of a rant: it bothers me that as developers, we preach iterative development and release, but when we're the customers of that approach the reaction is all too often "HOW DARE YOU PRESENT ME WITH SUCH INCOMPLETE IMPERFECTION".

As a developer, when I provide incomplete, incremental product to my client, I always says sorry to them until it's fully done. But as you mentioned, you want hear from clients "SHUTUP AND TAKE MY MONEY". This does not seems like apology.

Okay, here I am going to agree what "final" abilities of fetch are cool, really, very usable. What is wrong with it is that it misses basic API from the beginning, it's like that your skate but without wheels, only suspension and nice colors.

Main reason why I am against fetch is disability to cancel it. I understand it's "work in progress", but it's not usable at all without cancellation. My words "ugly" and "bad design" related mostly to it, because all proposals which comes up for cancellation are crutches, even those by myself.

Cancelable Promises

Promises nature is not about cancellation, they were designed to encapsulate async value (and its rejection/execution error). Promises do not provide external API to manipulate already live promise, reject it or fullfill. Is not that main goal of it? I believe yes, but cancel-ability breaks that goal. It requires external manipulation for promises. Let's take a look for "potential" Cancelable Promises design:

  var req = new Promise(function(fullfill, reject) {
    var xhr = new XMLHttpRequest();
    // ...

    return {
      cancel: function() {
        xhr.abort();
      }
    };
  });

  req
    .then(...)
    .then(...)
    .cancel(); // explicit manipulation

This is why Cancelable Promises are not designed yet (and I believe will never be because that will not be Promises). Here fetch team decided to implement their own way of canceling their requests, and, of course, Promises. There were a lot of ideas about how to do it, but let's focus on two most popular (last is what they are considering right now). Let me say ahead, this is most ugly part of fetch design.

FetchPromise, subclass of Promise

  var req1 = fetch(...);
  var req2 = fetch(...);

  FetchPromise.all([req1, req2]) // note: not Promise.all
    .then()
    .then()
    .cancel() // explicit manipulation

This way seems promising and I think people could live with addition promise constructor (FetchPromise). But, of course, this way will not work since fetch is not that simple, it has 3 stages, instead of 2 (as XHR for example).

  • Nothing done yet
  • FetchPromise resolved
  • All done

This means what after FetchPromise is resolved, there are still work do to and canceling that Promise will not help (you will need to cancel Stream manually).

FetchController

  var fetchController1 = new FetchContorller();
  var fetchController2 = new FetchController();

  var req1 = fetch(..., fetchController1);
  var req2 = fetch(..., fetchController2);

  // or
  /*var fetchController1;
  var fetchController2;

  var req1 = fetch(..., function(controller) {
    fetchController1 = controller;
  });
  var req2 = fetch(..., function(controller) {
    fetchController2 = controller;
  });*/

  Promise.all([req1, req2])
    .then()
    .then()
    .then(function() {
      [fetchController1, fetchController2].forEach(function(controller) {
        controller.cancel(); // Yay! Non explicit manipulation
      });
    });

This way is main candidate for success for now. Is not it nice? Nicely designed Promise based API without any hacks around it. Jake thinks I should explain this, but to be honest I do not know how. It's just ugly for me. I just do not want to define one more new object every time I write AJAX request with cancellation. I have hundreds of AJAX calls in one old social-network project, it by some circumstances uses jQuery and that is easy. I never needed to use Stream API in that real-live project, but I always needed to cancel previous request on page navigation (JS navigation, blah, blah). I do not like jQuery (or what it became), but AJAX call is very simple there (and usable), just as XHR.

One more thing to this paragraph: Jake asked me to explain what is "ugly" with fetch, but his arguments for a whole post were same as mine in twitter -- "XHR is bad", "XHR is ugly", "Fetch is not done yet, but XHR already defined in terms of fetch". Nothing concrete.

fetch vs XHR

As Jake says -- "fetch is massive improvement on XMLHttpRequest". I should disagree. It has only two differences -- Streams and Promises. And last is not an improvement.
(post edit: okay, it has more than that, "no-cors", etc. but all of that is just minor improvement)

Fetch way

fetch(url).then(function(response) {
  return response.json();
}).then(function(data) {
  console.log(data);
}).catch(function() {
  console.log("Booo");
});

XHR way

var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'json';

xhr.onload = function() {
  console.log(xhr.response);
};

xhr.onerror = function() {
  console.log("Booo");
};

xhr.send();

Absolutely. Clean. XHR is vomit. Why? Because it does not uses Promises, this cool new shine thing? Why all new standards are getting to define all with Promise? It should not be done that way, you have a choice. Many apps are not ready/designed to use Promises, even more, there are no tools for debugging promises (hopefully yet), but still. It's too in progress, but you are promoting it as a first and only one way to do async stuff. I can easily wrap XHR in Promise, will it shine the vomit?

var xhr = function(url) {
  return new Promise(function(resolve, resolve) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.onload = function() {
      resolve(xhr.response);
    };

    xhr.onerror = function() {
      reject();
    };

    xhr.send();
  });
};

xhr('...').then(function(response) {
  return JSON.parse(response);
}).then(function(data) {
  console.log(data);
}).catch(function() {
  console.log("Booo");
});

Streams... let's suppose XHR has Streams. Will it be that bad or bad at all?

var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'stream';

// or readyState === 3
xhr.onloading = function() {
  var ready = xhr.response.getReader();
  // ...
};

xhr.onerror = function() {
  console.log("Booo");
};

xhr.send();

What is the problem here? What Stream error might not propagate to xhr.onerror method? It should since that still will be request error. Or what canceling the stream will not cause xhr.abort? It should if Stream cancellation actually stops network request. Again, it should and it might work, but will never work with Promises. They are not about it.

But let's get back to fetch and things Jake did not mentioned...
Take a look again at a example under "Fetch way" title. As you see, fetch should be always followed by .then in the first place and handle response in some or another way and if you want to return request promise (to other function somewhere), then you should not return promise gotten by fetch() call itself, because of that middle state between nothing done and all done. What a hell? One may ask.. Yes, I will explain.

Once fetch promise is fullfiled or rejected it cannot change its state. So if there will be error in next stage, for example when Stream will break, then only that stream will know about it. Let's look at the example:

var req = doSomethingAndGetFetch(); // fetch promise here
// ...
// imagine  what it already fullfilled here, but after that its Stream got an error from server/network stack

req.then(function(response) {
  // then you will enter here, this function will be called anyway
  // this probably will not harm if you will be careful and do only
  // "return response.json()" form here to pipe
  /// then next promise in chain will be rejected
  // but you should remember -- never do here non fetch related stuff, please, remember

  return response.json();
}).then(function() {
  // this will not be called since previous promise in the chain will be rejected
}).catch(function() {
  // and then you are getting here ... do something!
});

Here probably nothing critical about it, but it's most likely will be one of most popular topics on StackOverflow.

How to fix

Get rid of "Promise must be used everywhere and returned from everything"
Eliminate "middle-state" and be more explicit about what we are doing with fetch

To be honest, there is not way to fix fetch at this state/design. But we it's possible to have some alternative way, with a lot of tricks too, but much less than current fetch has.

If someone is interested, here are my thoughts about other fetch design and API developed in a few hours. (I do not pretend what it should be implemented as a replacement, it just solves in some way those problems mentioned in this post)

In the end

As you may notice, I am not a "One particular high-profile JavaScript community member was unconvinced" or a spec's expert. I am a regular JavaScript/Front-end developer and this is how I see it from my point of view.

No wheels

@WebReflection
Copy link

you know what I've just discovered? https://xhr.spec.whatwg.org/#terminate-the-request the current XMLHttpRequest is under WHATWG now and they introduced Fetch in the send() logic at point 11 3 so actually using xhr.responseType = 'stream'; and retrieve xhr.response.getReader() on event loadstart would be the easiest, most straight forward, thing to do on planet earth because everything is already there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment