Fetch API: Building a Fetch Polyfill From Scratch (For Fun and Promise)

The Fetch API is a browser method for making AJAX requests. Learn how to build your own Fetch API polyfill from scratch and use ES2015 Promises like a pro.

Fetch API: Building a Fetch Polyfill From Scratch

What is the Fetch API?

Often overlooked in favor of libraries like Axios, the Fetch API is a native browser method for making AJAX requests. Fetch was born in 2015, the same year which saw the introduction of ECMAScript 2015 and Promise. But AJAX, a set of technologies for fetching data in the browser exists since 1999.

At that time the ability to fetch data from a web page without causing a page refresh was revolutionary. Nowadays we take AJAX and Fetch for granted but few know that Fetch is nothing more than a glorified version of XMLHttpRequest.

XMLHttpRequest in fact existed for almost 13 years and is the method that most JavaScript developers are familiar with. Given a url we can make an HTTP request like so:

var request = new XMLHttpRequest();
request.open("GET", "https://academy.valentinog.com/api/link/");
request.send();
request.addEventListener("load", function() {
  console.log(this.response);
});

Fetch is similar but looks more concise, and more important is based on Promises:

fetch("https://academy.valentinog.com/api/link/").then(function(response) {
  console.log(response);
});

But what’s a Promise by the way?

What is a JavaScript Promise?

Years have passed at the time of this writing since the introduction of ECMAScript 2015 Promise. You should already be familiar with Promises but here’s a quick recap.

A JavaScript Promise is the representation of a future event. A Promise may have 3 states:

  • pending, the default state
  • resolved, success state
  • rejected, error state

As a JavaScript developer most of the time you will “consume” a Promise rather than create one from scratch. That means most developers know about Promises because they’ve used then and catch as consumers of other people code.

But that’s only half of the story. Library creators on the other hand use Promise for wrapping legacy code and offering a more modern API to developers.

In the following exercise you’ll build Fetch from scratch, making use of Promises for wrapping XMLHttpRequest.

What is a polyfill?

“Polyfill” is an hand-made version of a functionality that is not yet implemented in all browsers.

In other words is custom code that any JavaScript developer can write and distribute, code that enables a new functionality in the language (or in browsers).

For example before the real Fetch API was released to the public, developers eager to try it installed a Fetch polyfill for enabling the feature. All of that before the “real” Fetch was implemented by browser’s vendors.

Setting up the project

To start off create a new file named fetch.html and save it in a folder of choice:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Building Fetch from scratch</title>
</head>
<body>

</body>
<script src="fetch.js"></script>
</html>

Then create another file named fetch.js in the same folder and start the code with the following statements:

"use strict";

window.fetch = null;

In the first line we ensure we’re in strict mode and in the second line we “nullify” the original Fetch API. And with this in place we can start building our polyfill.

Reverse engineering the Fetch API

The way Fetch works is quite simple. It takes an url and makes a GET request against it:

fetch("https://academy.valentinog.com/api/link/").then(function(response) {
  console.log(response);
});

When a function is “chainable” with then it means it’s returning a Promise. So inside fetch.js let’s create a function named fetch, which takes an url and returns a new Promise.

For creating a Promise you can call the Promise constructor and pass in a callback taking resolve and reject:

function fetch(url) {
  return new Promise(function(resolve, reject) {
    // do stuff
  });
}

We know that when a function returns a resolved Promise the then handler is triggered. Let’s give it a shot by resolving our Promise:

function fetch(url) {
  return new Promise(function(resolve, reject) {
    resolve("Fake response!");
  });
}

Try the complete code. Now we assign our fake fetch to window.fetch:

"use strict";

window.fetch = fetch;

function fetch(url) {
  return new Promise(function(resolve, reject) {
    resolve("Fake response!");
  });
}

fetch("https://academy.valentinog.com/api/link/").then(function(response) {
  console.log(response);
});

You should get “Fake response!” inside the console. It works!

Granted, it’s still a useless Fetch because nothing gets returned from the API. Let’s implement the real behaviour, with some help from XMLHttpRequest.

Implementing Fetch API GET requests

We saw how to create a request with XMLHttpRequest:

var request = new XMLHttpRequest();
request.open("GET", "https://academy.valentinog.com/api/link/");
request.send();
request.onload = function() {
  console.log(this.response);
};

for maximum compatibility we’re going to use the legacy onload for listening to the load event.

Let’s put the knowledge to good use. What we’re going to do now is wrapping XMLHttpRequest inside our Promise.

Open up fetch.js and extend the fetch function to include a new XMLHttpRequest:

function fetch(url) {
  return new Promise(function(resolve, reject) {
    var request = new XMLHttpRequest();
    request.open("GET", url);
    request.onload = function() {
      // handle the response
    };
    request.send();
  });
}

Things start to make sense now, isn’t it? We can also register an handler for failures on onerror:

function fetch(url) {
  return new Promise(function(resolve, reject) {
    var request = new XMLHttpRequest();
    request.open("GET", url);
    request.onload = function() {
      // handle the response
    };
    request.onerror = function() {
      // handle the error
    };
    request.send();
  });
}

At this point it’s a matter of resolving or rejecting the Promise depending on the request’s fate. That means resolving the Promise inside the “load” handler with the response we’ve got from the server:

function fetch(url) {
  return new Promise(function(resolve, reject) {
    var request = new XMLHttpRequest();
    request.open("GET", url);
    request.onload = function() {
      resolve(this.response);
    };
    request.onerror = function() {
      // handle the error
    };
    request.send();
  });
}

In case of error we reject the Promise inside the “error” handler:

function fetch(url) {
  return new Promise(function(resolve, reject) {
    var request = new XMLHttpRequest();
    request.open("GET", url);
    request.onload = function() {
      resolve(this.response);
    };
    request.onerror = function() {
      reject("Network error!");
    };
    request.send();
  });
}

Rejected Promises are handled by the catch handler, that means you’ll be able to use your “fake” Fetch like so:

fetch("https://acdemy.valentinog.com/api/link/")
  .then(function(response) {
    console.log(response);
  })
  .catch(function(error) {
    console.log(error);
  });

Now in case of an incorrect url our Fetch will print “Network error!” to the console. If the url is correct you’ll get the response from the server:

Fetch API: Building a Fetch Polyfill From Scratch - Server response

Fantastic! Our Fetch polyfill works but there are some rough edges. First of all we need to implement a function for returning JSON. Let’s see how.

Returning JSON from the Fetch API

The actual Fetch API yields a response which can be later converted to JSON, blob or text, like so (for the scope of this exercise we’ll implement only the JSON function):

fetch("https://academy.valentinog.com/api/link/")
  .then(function(response) {
    return response.json();
  })
  .then(function(json) {
    console.log(json);
  })

It should be easy to implement the functionality. Seems that “response” might be an entity on its own with a json() function attached.

The JavaScript prototype system is a good candidate for structuring the code (check out the secret life of JavaScript objects for a refresh). Let’s create a constructor function named Response and a method bound to its prototype (in fetch.js):

function Response(response) {
  this.response = response;
}

Response.prototype.json = function() {
  return JSON.parse(this.response);
};

That’s all! Now we can use Response inside Fetch:

function fetch(url) {
  return new Promise(function(resolve, reject) {
    var request = new XMLHttpRequest();
    request.open("GET", url);
    request.onload = function() {
      // Before:
      // resolve(this.response);
      // After:
      var response = new Response(this.response);
      resolve(response);
    };
    request.onerror = function() {
      reject("Network error!");
    };
    request.send();
  });
}

Here’s the complete code so far:

window.fetch = fetch;

function Response(response) {
  this.response = response;
}

Response.prototype.json = function() {
  return JSON.parse(this.response);
};

function fetch(url) {
  return new Promise(function(resolve, reject) {
    var request = new XMLHttpRequest();
    request.open("GET", url);
    request.onload = function() {
      // Before:
      // resolve(this.response);
      // After:
      var response = new Response(this.response);
      resolve(response);
    };
    request.onerror = function() {
      reject("Network error!");
    };
    request.send();
  });
}

fetch("https://academy.valentinog.com/api/link/")
  .then(function(response) {
    return response.json();
  })
  .then(function(json) {
    console.log(json);
  })
  .catch(function(error) {
    console.log(error);
  });

If you did everything correct the above code should print an array of objects into the browser’s console. Good job! And now let’s take care of errors.

Handling errors with the Fetch API

The real version of Fetch is much more complicated than our polyfill but it’s not so difficult to replicate (almost) the same exact behaviour.

The Response object in Fetch has a property, a boolean called “ok”. That boolean is set to true when the request succeeds and to false when the request fails. A developer can check the boolean and alt the Promise handler by throwing an error. Here’s how you would check the status with the real Fetch:

fetch("https://academy.valentinog.com/api/link/")
  .then(function(response) {
    if (!response.ok) {
      throw Error(response.statusText);
    }
    return response.json();
  })
  .then(function(json) {
    console.log(json);
  })
  .catch(function(error) {
    console.log(error);
  });

As you can see there’s also a “statusText”. Seems easy to implement both “ok” and “statusText” in the Response object. In particular response.ok should be true when the server replies:

  • with a status code equal or less than 200
  • with a status code less than 300

Open up fetch.js and tweak the Response object to include the new properties. Also make sure to change the first line to read the response from response.response:

function Response(response) {
  this.response = response.response;
  this.ok = response.status >= 200 && response.status < 300;
  this.statusText = response.statusText;
}

And guess what there’s no need to make up “statusText” because it’s already returning from the original XMLHttpRequest response object. That means we only need to pass the entire response when constructing our custom Response:

function fetch(url) {
  return new Promise(function(resolve, reject) {
    var request = new XMLHttpRequest();
    request.open("GET", url);
    request.onload = function() {
      // Before:
      // var response = new Response(this.response);
      // After: pass the entire response
      var response = new Response(this);
      resolve(response);
    };
    request.onerror = function() {
      reject("Network error!");
    };
    request.send();
  });
}

Here’s the complete code again:

"use strict";

window.fetch = fetch;

function Response(response) {
  this.response = response.response;
  this.ok = response.status >= 200 && response.status < 300;
  this.statusText = response.statusText;
}

Response.prototype.json = function() {
  return JSON.parse(this.response);
};

function fetch(url) {
  return new Promise(function(resolve, reject) {
    var request = new XMLHttpRequest();
    request.open("GET", url);
    request.onload = function() {
      // Before:
      // var response = new Response(this.response);
      // After: pass the entire response
      var response = new Response(this);
      resolve(response);
    };
    request.onerror = function() {
      reject("Network error!");
    };
    request.send();
  });
}

fetch("https://academy.valentinog.com/api/link/")
  .then(function(response) {
    if (!response.ok) {
      throw Error(response.statusText);
    }
    return response.json();
  })
  .then(function(json) {
    console.log(json);
  })
  .catch(function(error) {
    console.log(error);
  });

As of now our Fetch polyfill is able to handle errors and success. You can check the error handling by providing a failing url like so:

fetch("https://httpstat.us/400")
  .then(function(response) {
    if (!response.ok) {
      throw Error(response.statusText);
    }
    return response.json();
  })
  .then(function(json) {
    console.log(json);
  })
  .catch(function(error) {
    console.log(error);
  });

You should see “Error: bad request” as expected in the browser’s console. Way to go! And now let’s extend our polyfill for making POST requests.

Implementing Fetch API POST requests

First of all let’s see how to make POST requests with the real Fetch. The entity we’re going to create on the server is a “link”, that is, made of a title and an url:

var link = {
  title: "Building a Fetch Polyfill From Scratch",
  url: "https://www.valentinog.com/fetch-api/"
};

Next up we configure the request for Fetch. It’s done in an object which should contain at least the HTTP method, the request’s content type (JSON in our case) and the request’s body (the new link):

var link = {
  title: "Building a Fetch Polyfill From Scratch",
  url: "https://www.valentinog.com/fetch-api/"
};

var requestInit = {
  method: "POST",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify(link)
};

Finally we make the request like so:

var link = {
  title: "Building a Fetch Polyfill From Scratch",
  url: "https://www.valentinog.com/fetch-api/"
};

var requestInit = {
  method: "POST",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify(link)
};

fetch("https://academy.valentinog.com/api/link/create/", requestInit)
  .then(function(response) {
    if (!response.ok) {
      throw Error(response.statusText);
    }
    return response.json();
  })
  .then(function(json) {
    console.log(json);
  })
  .catch(function(error) {
    console.log(error);
  });

But now we have a problem with our polyfill. It accepts a single parameter “url” and makes only GET requests against it:

function fetch(url) {
  return new Promise(function(resolve, reject) {
    var request = new XMLHttpRequest();
    request.open("GET", url);
    request.onload = function() {
      var response = new Response(this);
      resolve(response);
    };
    request.onerror = function() {
      reject("Network error!");
    };
    request.send();
  });
}

Fixing the issue should be easy. First we can accept a second parameter named requestInit. Then based on that parameter we can construct a new Request object that:

  • makes GET requests by default
  • uses body, method, and headers from requestInit if the latter is provided

To start with we make a new “Request” function with some properties named body, method, and headers. Open up fetch.js and get coding:

function Request(requestInit) {
  this.method = requestInit.method || "GET";
  this.body = requestInit.body;
  this.headers = requestInit.headers;
}

Next up we’re going to use the new function inside our polyfill. We prepare a request configuration object just before opening the request:

function fetch(url, requestInit) {
  return new Promise(function(resolve, reject) {
    var request = new XMLHttpRequest();
    var requestConfiguration = new Request(requestInit || {});
    // more soon
  });
}

Now the request will use the method defined in the Request object:

function fetch(url, requestInit) {
  return new Promise(function(resolve, reject) {
    var request = new XMLHttpRequest();
    var requestConfiguration = new Request(requestInit || {});
    request.open(requestConfiguration.method, url);
    // more soon
  });
}

The logic for handling errors and success (load) remains the same:

function fetch(url, requestInit) {
  return new Promise(function(resolve, reject) {
    var request = new XMLHttpRequest();
    var requestConfiguration = new Request(requestInit || {});
    request.open(requestConfiguration.method, url);
    request.onload = function() {
      var response = new Response(this);
      resolve(response);
    };
    request.onerror = function() {
      reject("Network error!");
    };
    // more soon
  });
}

But on top of that we can add a minimal logic for setting up request headers:

function fetch(url, requestInit) {
  return new Promise(function(resolve, reject) {
    var request = new XMLHttpRequest();
    var requestConfiguration = new Request(requestInit || {});
    request.open(requestConfiguration.method, url);
    request.onload = function() {
      var response = new Response(this);
      resolve(response);
    };
    request.onerror = function() {
      reject("Network error!");
    };
    // Set headers on the request
    for (var header in requestConfiguration.headers) {
      request.setRequestHeader(header, requestConfiguration.headers[header]);
    }
    // more soon
  });
}

Headers are important for configuring the AJAX request. Most of the times you may want to set application/json or an authentication token in the headers.

As the cherry on top we can complete our code to send:

  • an empty request if there is no body
  • a request with some payload is the body is provided:
function fetch(url, requestInit) {
  return new Promise(function(resolve, reject) {
    var request = new XMLHttpRequest();
    var requestConfiguration = new Request(requestInit || {});
    request.open(requestConfiguration.method, url);
    request.onload = function() {
      var response = new Response(this);
      resolve(response);
    };
    request.onerror = function() {
      reject("Network error!");
    };
    // Set headers on the request
    for (var header in requestConfiguration.headers) {
      request.setRequestHeader(header, requestConfiguration.headers[header]);
    }
    // If there's a body send it
    // If not send an empty GET request
    requestConfiguration.body && request.send(requestConfiguration.body);
    requestConfiguration.body || request.send();
  });
}

That should cover the POST request feature. Here’s the complete code for our Fetch polyfill so far:

"use strict";

window.fetch = fetch;

function Response(response) {
  this.response = response.response;
  this.ok = response.status >= 200 && response.status < 300;
  this.statusText = response.statusText;
}

Response.prototype.json = function() {
  return JSON.parse(this.response);
};

function Request(requestInit) {
  this.method = requestInit.method || "GET";
  this.body = requestInit.body;
  this.headers = requestInit.headers;
}

function fetch(url, requestInit) {
  return new Promise(function(resolve, reject) {
    var request = new XMLHttpRequest();
    var requestConfiguration = new Request(requestInit || {});
    request.open(requestConfiguration.method, url);
    request.onload = function() {
      var response = new Response(this);
      resolve(response);
    };
    request.onerror = function() {
      reject("Network error!");
    };
    for (var header in requestConfiguration.headers) {
      request.setRequestHeader(header, requestConfiguration.headers[header]);
    }
    requestConfiguration.body && request.send(requestConfiguration.body);
    requestConfiguration.body || request.send();
  });
}

var link = {
  title: "Building a Fetch Polyfill From Scratch",
  url: "https://www.valentinog.com/fetch-api/"
};

var requestInit = {
  method: "POST",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify(link)
};

fetch("https://academy.valentinog.com/api/link/create/", requestInit)
  .then(function(response) {
    if (!response.ok) {
      throw Error(response.statusText);
    }
    return response.json();
  })
  .then(function(json) {
    console.log(json);
  })
  .catch(function(error) {
    console.log(error);
  });

Give it a shot and maybe write a test for it!

Fetch API: Building a Fetch Polyfill From Scratch. Wrapping up

The real Fetch API implementation is much more complex and has support for more advanced features. We just scratched the surface of Fetch in this tutorial but as you can see it’s coming up quite well.

Of course the code can be improved, for example the logic for adding headers can live on a method on it’s own. Plus there’s a lot of room for adding new features: support for all HTTP methods and more functions for returning the response in different formats.

I leave that as an exercise for you. If you’re curious to see a more complex Fetch API polyfill check out whatwg-fetch from the engineers at Github. You’ll see a lot of similarities with your handcrafted polyfill!

I really hope you learned something new in this tutorial and that you’ll be able to build the next amazing polyfill for the web platform.

Thanks for reading and stay tuned!

One Reply to “Fetch API: Building a Fetch Polyfill From Scratch (For Fun and Promise)”

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.