Introduction

I wanted to write a shorti(ish) post mostly for myself about how to use async/await on in the browser. It's one of those things that no matter how many often I use it, I feel like I have to start from scratch every time. Hopefully by writing this I'll clarify it for myself and perhaps others who have a similar problem.

Setup

I wanted to keep my example as straightforward as possible. To me, that means it should:

  • have very few DOM elements
  • use no frameworks
  • use at most one interaction

The point isn't to be fancy or even pretty. The point is only to make sure I understand the concept. To do that I came up with the following idea.

The user should be able to click a button and have a random image display in the browser.

Excellent. One button tag, one image tag, and one click event listener.

HTML

The markup for this is going to be very straightforward.

<div>
  <img src='' />
  <button>Get a random image</button>
</div>

Note that the image tag's source attribute is empty. That's what I'll fill in with javascript. Speaking of which...

Javascript

To get the image, I'm going to use unsplash's random image endpoint. It's perfect for this because

  • it doesn't require an API key
  • it will return an image url
  • for now I don't care about anything else

Variables

First, I'll define a few variables.

const img = document.querySelector('img')
const button = document.querySelector('button')
const div = document.querySelector('div')
const url = 'https://source.unsplash.com/random'

Event Listener

Next, before I get really complicated, I like to write my event listener and log a click event. That way I know at least that works correctly before getting into the "hard part".

const img = document.querySelector('img')
const button = document.querySelector('button')
const div = document.querySelector('div')
const url = 'https://source.unsplash.com/random'

button.addEventListener('click', () => {
  console.log('click');
});

async/await

Now comes the part I care about. What I want is to fetch the image asynchronously. Then, I want to use the source url of the result as the src attribute of the image.

To do that, I want to write an async function and have it return the awaited result. To get the response from the endpoint, I'll use the fetch API.

Let's call the function getImage.

const img = document.querySelector('img')
const button = document.querySelector('button')
const div = document.querySelector('div')
const url = 'https://source.unsplash.com/random'

const getImage = async (url) => {
  return await fetch(url).then(res => res.url)
}

button.addEventListener('click', () => {
  console.log('click');
});

Now when the button is clicked, the getImage function can be run. But, that's the "tricky" part. It's not sufficient to just call the function. That will leave the Promise returned by the function in a pending state. You have to do something with the result of the function. That's done by chaining a .then function on to it. This is where I can set the attribute of the image tag.

const img = document.querySelector('img')
const button = document.querySelector('button')
const div = document.querySelector('div')
const url = 'https://source.unsplash.com/random'

const getImage = async (url) => {
  return await fetch(url).then(res => res.url)
}

button.addEventListener('click', () => {
  getImage(url)
    .then(result => {
      img.setAttribute('src', result)
    })
});

Handling failure

The last thing that needs to happen for this little example is some kind of error handling. Right now if something goes wrong, the person clicking the button won't know. To handle that, I want to catch the error, log it, and display a message.

const img = document.querySelector('img')
const button = document.querySelector('button')
const div = document.querySelector('div')
const url = 'https://source.unsplash.com/random'

const getImage = async (url) => {
  return await fetch(url).then(res => res.url)
}

button.addEventListener('click', () => {
  getImage(url)
    .then(result => {
      img.setAttribute('src', result)
    })
    .catch(err => {
      // log the specific error
      console.error(`Error -> ${err}`)
      const errorMessage = 'Sorry, but there was an error getting your image.'
      const p = document.createElement('p')
      const container = document.querySelector('.container')
      p.innerText = errorMessage;
      //display an error message to the user
      container.appendChild(p)
    })
});

Conclusion

I like doing small examples like this. They're a good way for me to focus on the core problem I want to solve and not get distracted with other issues. Here, the main thing was to have a reminder for myself of how to create an asynchronous browser interaction and handle a successful or unsuccessful response without getting too wrapped up in intracacies.