DEV Community

Cover image for ES6 - A beginners guide - Promises and Fetch
Stefan Wright
Stefan Wright

Posted on • Updated on

ES6 - A beginners guide - Promises and Fetch

This time I am going to cover ES6's introduction of Promise and Fetch as native JavaScript functionality in the browser. A lot of dev's will use 3rd party libraries such as Axios, SuperAgent, or jQuery although it might not always be necessary to do so and it may just add bloat to your projects. We'll start by looking at ES6 Promises, before heading on over to details about Fetch

Promises

What is a Promise?

Much like in the real world, a promise is the result of saying we will do something and give something back. Let's say we wanted to run this piece of code:

const url = "http://www.json.com";
const data = makeRequest(url);
console.log(data);
Enter fullscreen mode Exit fullscreen mode

In the above, our console.log will result in showing undefined because we will simply be executing line 3 immediately after line 2, regardless of how quick that makeRequest function runs, it will never be quicker than the execution of the following line. JavaScript Promises give us a method of using 3 different states whilst waiting for something to complete, such as an AJAX request. The three states were can use are:

  • unresolved - This is out "waiting" phase, if we were to check in on the value of a Promise periodically using a setTimeout() or similar we would see this until the promise either completed or failed
  • resolved - This is our "finished" state, we have finished getting the data, the promise is fulfilled and something is ready to be returned.
  • rejected - This is our "error" state, something went wrong, this would be used to trigger some form of error handling. Off the back of these states we have two possible callbacks that we can use:
  • then - This can be used after a resolved state is triggered, it tells our code what to do next
  • catch - Much like with a try/catch loop, this is where we perform our error handling ### How about an example? The following examples, can easily be plugged straight into the Dev Tools of your browser and run from the Console screen. Let's get started:
promise = new Promise()
Enter fullscreen mode Exit fullscreen mode

Uh oh! we got an error, but why? well, if you run the above code you should see an error similar to Uncaught TypeError: Promise resolver undefined is not a function. This error response is telling us that the browser knows what a Promise is, but we haven't told it what to do in order to resolve the promise. It's actually really simple to fix. Let's fix it now:

promise = new Promise(()=>{})
Enter fullscreen mode Exit fullscreen mode

Now we have created a promise, if you run the above code you'll see that it gives a response similar to this:
Promise {<pending>}[[Prototype]]: Promise[[PromiseState]]: "pending"[[PromiseResult]]: undefined. So now we have created a promise, it doesn't do much right now though. When we define a promise we need to handle how/when its resolved and rejected, luckily the Promise had two built in arguments that we can use, these are resolve and reject. Let's have a look at that:

promiseResolve = new Promise((resolve, reject) => {
  resolve()
});
Enter fullscreen mode Exit fullscreen mode

In the code above you'll see we create a new Promise, we include our two arguments in our inner function. We then call resolve() inside our function in order to complete the execution. If you run the code above the browser will output something like: Promise {<fulfilled>: undefined}. Likewise we can do the same with reject():

promiseReject = new Promise((resolve, reject) => {
  reject()
});
Enter fullscreen mode Exit fullscreen mode

Wait! we got a warning, we have the following returned Promise {<rejected>: undefined} this is expected, however we also got Uncaught (in promise) undefined because we didn't handle the rejection properly. Let's look at our callbacks, they'll help us handle both state calls above.

Using callbacks

ES6 Promises give us two built in callback methods as mentioned above they are .then() and .catch(). We can use .then() when we resolve a promise to instruct our code on the next action, and the parameter in the function will automatically take the value that was returned in our promise. Let's look at an example:

promiseResolve = new Promise((resolve, reject) => {
  resolve('Promise resolved');
});

promiseResolve
  .then((resolvedValue) => console.log(resolvedValue))
  .then(() => console.log('Still resolved'))
Enter fullscreen mode Exit fullscreen mode

Notice how in our first .then() we have a parameter for our function, we then use that parameter in the return of the arrow function, however rather than giving us an error about the variable being undefined the above code will actually give use the following output:

Promise resolved
Still resolved
Promise {<fulfilled>: undefined}
Enter fullscreen mode Exit fullscreen mode

So as we can see resolvedValue actually gives us the value we passed back in the resolve. We're going to revisit this later in the article when we look at using fetch to pull remote data. Now on to error handling, let's jump straight into an example:

promiseReject = new Promise((resolve, reject) => {
  reject('Promise rejected')
});

promiseReject
  .then(() => console.log('Promise resolved'))
  .then(() => console.log('Still resolved'))
  .catch((err) => console.log(err))
Enter fullscreen mode Exit fullscreen mode

As with the above, we can now see that our catch is including a parameter and our console.log message contains Promise rejected but we do not output Promise resolved or Still resolved and this is because we fired the reject() line in out promise.

Using asyncronous callbacks

We can use asyncronous callback in our ES6 Promises, this can help to simulate what would happen when making an AJAX call or similar to pull data. In the example below we will wait for 5 seconds before resolving our promise.

promiseAsync = new Promise((resolve, reject) => {
  console.log('Starting Promise...')
  setTimeout(() => {resolve('Promise resolved')}, 5000)
});

promiseAsync
  .then((response) => console.log(response))
  .catch(() => console.log('Promise rejected'))
Enter fullscreen mode Exit fullscreen mode

We can use a library like jQuery to make a request and using a promise we can then take an action when it completes, take a look below, we will add a log to say we have started, then we'll fetch a JSON sample of blog posts in a promise, and then log that response

promiseAsync = new Promise((resolve, reject) => {
  console.log('Starting promise')
  $.ajax({
    url: 'https://jsonplaceholder.typicode.com/posts/',
    type: 'GET',
    success: function (data) {
      resolve(data)
    },
    error: function (error) {
      reject(error)
    },
  })
})

promiseAsync
  .then((response) => console.log(response))
  .catch((error) => console.log('Promise rejected', error))
Enter fullscreen mode Exit fullscreen mode

Running the code above gives us Starting promise and then Array(100) in the dev tools. I ran this on JSFiddle so that I could ensure jQuery was included.

So what about Fetch?

Fetch is a new feature included with ES6, it provides us to combine a network request with a promise in a super simple form! It does have its limitations though, and i'll go into them in a bit, but first...you know what's coming...an example!

const url = "https://jsonplaceholder.typicode.com/posts/";
fetch(url)
Enter fullscreen mode Exit fullscreen mode

The code above will simply give us a pending Promise in the browser, that's no use to us in this state. With a Promise, as above, we would have to supply a function to handle the resolve/reject conditions, fetch does this for us though. All we need to do is supply callbacks

const url = "https://jsonplaceholder.typicode.com/posts/";
fetch(url)
  .then(data => console.log(data))
Enter fullscreen mode Exit fullscreen mode

Well, we're getting there, we now get the following output when we use this in the Console window:

Promise {<pending>}
Response {type: 'cors', url: 'https://jsonplaceholder.typicode.com/posts/', redirected: false, status: 200, ok: true, …}
Enter fullscreen mode Exit fullscreen mode

This doesn't give us the actual response data, just some metadata. If we want to extract the actual information from jsonplaceholder we are going to pass this through a different function first, let's take a look:

const url = "https://jsonplaceholder.typicode.com/posts/";
fetch(url)
  .then(response => response.json())
  .then(data => console.log(data))
Enter fullscreen mode Exit fullscreen mode

Above, we are first using .json() in order to take the response stream and render the response body text as JSON (MDN Documentation), we then call .then() again, this time taking in the return value from response.json() and then passing that to console.log. This gives us the following output:

Promise {<pending>}
(100) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
Enter fullscreen mode Exit fullscreen mode

But Stefan, you mentioned some downfalls of Fetch??

That's right, I did. I mentioned that we can use .then() and .catch() with Fetch, however the .catch() handler isn't always triggered when you expect it. For example, if you go to a page/endpoint that does not exist and gives you an HTTP404 response you won't actually hit the .catch() code

const badUrl = "https://jsonplaceholder.typicode.com/posts1321654646186/";
fetch(badUrl)
  .then(response => console.log('SUCCESS: ',response))
  .catch(error => console.log('ERROR', error))
Enter fullscreen mode Exit fullscreen mode

The code above will give ue the following output:

Promise {<pending>}
GET https://jsonplaceholder.typicode.com/posts1321654646186/ 404 (Not Found)
SUCCESS: Response {type: 'cors', url: 'https://jsonplaceholder.typicode.com/posts1321654646186/', redirected: false, status: 404, ok: false, …}
Enter fullscreen mode Exit fullscreen mode

We can see we got a 404 response, but the output is from our .then() callback. Fetch is designed in such a way that you would only hit the .catch() callback is there was a network level error (such as a failed DNS lookup). The following example would actually go to the .catch() callback:

const badUrlHost = "https://jsonplaceholder.typicode12345.com/posts/";
fetch(badUrlHost)
    .then(response => console.log('SUCCESS: ', response))
    .catch(error => console.log('ERROR', error))
Enter fullscreen mode Exit fullscreen mode

This time our console gives us:

Promise {<pending>}
GET https://jsonplaceholder.typicode12345.com/posts/ net::ERR_TUNNEL_CONNECTION_FAILED
ERROR TypeError: Failed to fetch at <anonymous>:2:1
Enter fullscreen mode Exit fullscreen mode

This is ok, but we still want to handle HTTP4XX or HTTP5XX errors gracefully

There is a way around this

There are generally a couple of suggested "workarounds" for working with these kinds of requests. If you NEED to use the Fetch API, then the following kind of code construction would be better for you:

const badUrl = "https://jsonplaceholder.typicode.com/posts1321654646186/";
fetch(badUrl)
  .then(response => {
    if(!response.ok){
        throw new Error("I'm an error");
    } else{
        return response.json()
    }
  })
  .then(data => console.log('Response Data', data))
  .catch(error => console.log('ERROR', error))
Enter fullscreen mode Exit fullscreen mode

In the code above we are now throwing an exception because the response metadatas property for ok was false.

Promise {<pending>}
GET https://jsonplaceholder.typicode.com/posts1321654646186/ 404 (Not Found)
ERROR Error: I'm an error
Enter fullscreen mode Exit fullscreen mode

From the metadata we could use either .ok or .status if we wanted to handle errors differently for HTTP4xx errors as opposed to HTTP5XX errors (for example), or we could use both for generic error handling, such as below:

const badUrl = "https://jsonplaceholder.typicode.com/posts1321654646186/";
fetch(badUrl)
  .then(response => {
    if(!response.ok){
        throw new Error(`${response.status} - ${response.statusText}`);
    } else{
        return response.json()
    }
  })
  .then(data => console.log('Response Data', data))
  .catch(error => console.log('ERROR', error))
Enter fullscreen mode Exit fullscreen mode

Now we see the following error output:

Promise {<pending>}
GET https://jsonplaceholder.typicode.com/posts1321654646186/ 404 (Not Found)
ERROR Error: 404 - Not Found
Enter fullscreen mode Exit fullscreen mode

I mentioned that there were a couple of suggestions for workarounds, alternatives to the above would be using 3rd Party Libraries/Tools such as:

You could also just use XMLHttpRequest which has long been baked into browsers as default functionality, information on that can be found on MDN here

Discussion (0)