Error Handling

Open the project folder

In our starter files, open the index.html page from this lessons folder:

10.Async-JavaScript > 08.Error-handling

Introduction

In this lesson, we will discover some ways to handle errors in your async code. Code is great when things go well, but when things don’t, we need to handle what to do.

Some of the Promise methods we have looked at have given us some error handling. For example, the all() method fails if one promise fails, so we have some options there.

But we also need to handle all other situations too, either a general error handler, or handling each specific request.

Project starter code

In the starter project, we have an empty image element, and a single function:

<body>
  <img src="" alt="" />
  <script>
    async function getImageUrl() {
      const response = await fetch('https://dog.ceo/api/breeds/image/random');
      const image = await response.json();
      return image.message;
    }
  </script>
</body>

Nothing new here for this function, it fetches the random image using async / await, extracts the image URL, and returns it from the function.

Async functions

Just before we use this, take a look at a simple example:

function hey() {
  return 'hey';
}
console.log(hey()); // hey

Nothing unexpected here. But if we mark this function as async:

async function hey() {
  return 'hey';
}
console.log(hey());

We now see a Promise instead of a string:

Promise {<fulfilled>: 'hey'}

Remember, async / await is built on promises too, and as soon as we mark a function as async, it will also return a Promise. Just bear this in mind for a moment.

Create a setImage() function

Back to the starter code, we have the function getImageUrl() to return the random image. Let’s create another function to set the image in the browser:

// ...
async function setImage() {
  // the getImageUrl function is async, so we await the promise
  document.querySelector('img').src = await getImageUrl();
}
setImage()

Catching errors

Earlier when handling errors using promises, we chained a catch block. This setImage function is async and therefore returning a promise, meaning we can use a catch block:

setImage()
.catch(function (error) {
  console.log('there was an error');
  console.log(error);
});

📝 Remember, when chaining any method, the semi-colon must be removed from the previous line.

If all is working, you should see the image in the browser. To test out the catch, we can switch off the network in the developer tools as we have done previously.

In Chrome, there will be an option in the network tab:

Chrome network tab

Set the offline option and you should see an error in the console:

⚠️ TypeError: Failed to fetch

Here we are mixing the syntax of async / await and promises. If we wanted to stick with the promise syntax, or move the error handling into the function itself, we could use try / catch.

The try / catch statement

With try / catch, we try to do something, and if it works that is great. If not, we catch the error and handle it how we want to. To see this, remove the existing catch block we just added. Your code should now look like this:

async function getImageUrl() {
  const response = await fetch("https://dog.ceo/api/breeds/image/random");
  const image = await response.json();
  return image.message;
}
async function setImage() {
  document.querySelector("img").src = await getImageUrl();
}
setImage();

Inside the setImage function, we can add a try and a catch block:

async function setImage() {
  try {}
  // catch is passed the error message
  catch (error) {}
  document.querySelector('img').src = await getImageUrl();
}

Then move the code we want to run inside the try block:

async function setImage() {
  try {
    document.querySelector('img').src = await getImageUrl();
  } catch (error) {}
}
setImage();

And then handle the error with catch:

catch (error) {
  console.log('there was an error');
  console.log(error);
}

Give this a try with the network on and off. You should see the error messages in the console when the network is offline.

This try / catch is run synchronously. Therefore it will run the try section first, and if it fails, it will run the catch block.

The finally block

We also have the finally block. This works just like when we chained finally onto a promise. This will always run regardless of if the promise was fulfilled or rejected:

async function setImage() {
  try {
    document.querySelector('img').src = await getImageUrl();
  } catch (error) {
    console.log('there was an error');
    console.log(error);
  } finally {
    console.log('always runs');
  }
}

One of either the catch or finally blocks needs to be present, or we can use both as we have here. This is fine for general error catching. But what about if we wanted to know which part of the try code failed?

Error handler function

For this we would chain a catch block directly onto an async task. First, remove all the error handling from setImage, leaving this:

async function setImage() {
  document.querySelector('img').src = await getImageUrl();
}

Then, chain the catch method inside the function:

async function setImage() {
  document.querySelector("img").src = await getImageUrl().catch();
}

We could pass a function directly into catch(), or if we wanted to re-use it, we could make a separate function:

// create error handler function
function handleError(error) {
  console.log('there was an error');
  console.log(error);
}
async function setImage() {
  // pass error handler to catch
  document.querySelector('img').src = await getImageUrl().catch(handleError);
}
setImage();

This function could then be re-used on multiple promises.

Of course, a console log wouldn’t be enough in a real app. We would want to maybe hide the image if there was an error, or add a placeholder, but the key is to do something rather than see the app or website break.

Handling errors is big part of async JavaScript, and we see some common patterns here which you can use in your projects to improve the functionality and user experience.