The JavaScript fetch API has been around for a few years now, but a couple of browsers (Safari and iOS Safari, for example) have only supported it for a little over a year now. Prior to fetch, we used XMLHttpRequest to request data in JavaScript. If you're a bit late to the game because you're still supporting some of those older browser versions, here's a step-by-step guide for how XMLHttpRequest can be repackaged into the fetch API.

Let's start with a simple XMLHttpRequest example. We have a GET request using a particular URL. (For the sake of brevity, I'm not attempting to handle all possible error states in these examples.)

let xhr = new XMLHttpRequest();
xhr.addEventListener("load", () => {
    console.log(xhr.responseText);
});
xhr.open("GET", "/assets/post-data/xhr.txt");
xhr.send();

We can package this up into a nice function that accepts two parameters - the URL and a callback function with the response.

function getRequest(url, callback) {
    let xhr = new XMLHttpRequest();
    xhr.addEventListener("load", () => {
        callback(xhr.responseText);
    });
    xhr.open("GET", url);
    xhr.send();
}

getRequest("/assets/post-data/xhr.txt", response => {
    console.log(response);
});

Using our getRequest function becomes a very convenient way to request data. However, we're assuming we won't run into any error cases with our request. What if we wanted to support another callback in case there's an error?

function getRequest(url, success, failure) {
    let xhr = new XMLHttpRequest();
    xhr.addEventListener("load", () => {
        success(xhr.responseText);
    });
    xhr.addEventListener("error", event => {
        failure(event);
    });
    xhr.open("GET", url);
    xhr.send();
}

getRequest("/assets/post-data/xhr.txt", response => {
    console.log(response);
}, error => {
    console.log(error);
});

Now we have something very similar to a Promise. We call our getRequest function and execute either the success or failure callback. If our function actually returned a Promise, calling it would look something like this:

getRequest("/assets/post-data/xhr.txt")
    .then(response => {
        console.log(response);
    })
    .catch(error => {
        console.log(error);
    });

And we would implement our function to return a Promise by wrapping its current implementation within a Promise.

function getRequest(url, success, failure) {
    return new Promise((resolve, reject) => {
        let xhr = new XMLHttpRequest();
        xhr.addEventListener("load", () => {
            resolve(xhr.responseText); // success function
        });
        xhr.addEventListener("error", event => {
            reject(event); // failure function
        });
        xhr.open("GET", url);
        xhr.send();
    });
}

Just rename getRequest to fetch and you have a very basic implementation of the fetch API. You can go back and add the necessary error handling as well as support for passing in request options and receiving more complex Response objects that more fully describe the HTTP response.

fetch("/assets/post-data/xhr.txt")
    .then(response => {
        return response.text();
    })
    .then(text => {
        console.log(text);
    })
    .catch(error => {
        console.log(error);
    });

Congratulations! You should now be able to see how we moved from XMLHttpRequest to fetch.