Lesson Wednesday

Note: This lesson is a further exploration. You will not be expected to chain promises together for this section's independent project. However, we recommend that you follow along with this lesson closelly because you'll learn more about coding best practices including using services, handling errors, and other things we can do to clean up our code.

In this lesson, we'll update our weather API project to chain together multiple promises. We've already chained together two promises with the Fetch API - but let's take it to another level by first making an API call to the Open Weather API, waiting on the results of that call, and then using the result of that call to make a second API call to the Giphy API. Processing data through multiple APIs is a real world use case and this is a great opportunity to practice chaining promises as well. In the process, we'll also have a chance to look at error handling for a more complex example, too.

Our updated application will display a weather description for the user and then show the user a GIF based on that description. There will be a lot of changes to the code - including a lot of refactoring to make it cleaner.

We already have code for our WeatherService, so what should we do for our Giphy code? Do you think it would be better to create a separate service in a different file called GiphyService or would it be better to turn WeatherService into ApiService and put all the API calls in there?

Think about it for a moment... and think about why you arrived at your choice. You will have to make decisions about separating code all of the time. Sometimes there will be a right or a wrong approach. And sometimes both approaches will have their advantages and disadvantages. But regardless of the case, you should always be prepared to think through how you structure your code.

If you think the best approach is two separate services, you are correct! The Giphy API and the OpenWeather API do two separate things. We want to keep our code separate and modular. We might even want to use each of these APIs elsewhere in our application in ways that have nothing to do with each other. Also, there is a trend in programming towards microservices. This is a software design pattern where applications are built around lots of smaller services that communicate with each other. In contrast, there's the monolithic approach which can have lots and lots of closely entangled code - which means that when things break, they really break. Microservices allow code to be loosely coupled, which means it isn't too dependent on other code. By separating out our API calls in two different services, they are fully decoupled and don't know about each other. If there's an error in one of our services, it won't break the other service (though it will certainly break any code that does depend on the broken service).

Let's create a separate directory in src for services and add both our weather-service.js file and a new giphy-service.js file. VSCode should automatically offer to update the paths in any import statements that use weather-service.js in your application. Click yes to do so or manually update the paths as needed.

Here's the code for our new GiphyService. It will be very similar to code we've written previously for using the Fetch API with promises:

src/services/giphy-service.js

export default class GiphyService {  
  static async getGif(query) {
    return fetch(`http://api.giphy.com/v1/gifs/search?q=${query}&api_key=${process.env.GIPHY_API_KEY}&limit=5`)
      .then(function(response) {
      if (!response.ok) {
        throw Error(response.status);
      }
      return response.json();
    })
    .catch(function(error) {
      return Error(error);
    }) 
  }
}

Most of this code should look familiar but there are a couple of important things to note. First, we use a generic parameter called query in our static method. We could use this API call anywhere in our application for things other than weather - so we don't want to call it something constricting like currentWeather.

Next, we name our environmental variable GIPHY_API_KEY. We'll also update the environmental variable name for the OpenWeather API to OPEN_WEATHER_API_KEY. Now that we are working with multiple APIs in our application, we need to make sure the names for each are descriptive. Make sure to update your .env file accordingly.

There's one other key change that's very important. If our application has any errors, our catch block will return an error. We'll see why this is important soon.

We'll also make some small updates to our WeatherService as well:

src/services/weather-service.js

export default class WeatherService {  
  static getWeather(city) {
    return fetch(`http://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${process.env.OPEN_WEATHER_API_KEY}`)
      .then(function(response) {
        if (!response.ok) {
          throw Error(response.statusText);
        }
        return response.json();
      })
      .catch(function(error) {
        return Error(error);
      });
  }
}

There's really just two small changes and we discussed them already with our new GiphyService: we've updated the name of the environmental variable and our catch block is now returning an error.

So now we have two services. Each returns either a promise that will resolve with API data or, if something goes wrong, returns an Error object.

Now we are ready to update main.js to handle our promises and chain them together. There will be quite a few updates to this file, including some refactoring. We'll also be making some updates to HTML classes in index.html and our updated UI methods will reflect that.

src/main.js
// Import statements updated to reflect new paths and also import GiphyService.
import $ from 'jquery';
import 'bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import './css/styles.css';
import WeatherService from './services/weather-service.js';
import GiphyService from './services/giphy-service';


// clearFields() updated to reflect changed HTML.
function clearFields() {
  $('#location').val("");
  $('.show-errors').text("");
}

// The three remaining UI functions make our code more modular. We'll discuss them soon.

function displayWeatherDescription(description) {
  $('.weather-description').text(`The weather is ${description}!`);
}

function displayGif(response) {
  const url = response.data[0].images.downsized.url;
  $('.show-gif').html(`<img src='${url}'>`);
}

function displayErrors(error) {
  $('.show-errors').text(`${error}`);
}

$(document).ready(function() {
  $('#weatherLocation').click(function() {
    let city = $('#location').val();
    clearFields();
    WeatherService.getWeather(city)
      .then(function(weatherResponse) {
        if (weatherResponse instanceof Error) {
          throw Error(`OpenWeather API error: ${weatherResponse.message}`);
        }
        const weatherDescription = weatherResponse.weather[0].description;
        displayWeatherDescription(weatherDescription);
        return GiphyService.getGif(weatherDescription);
      })
      .then(function(giphyResponse) {
        if (giphyResponse instanceof Error) {
          throw Error(`Giphy API error: ${giphyResponse.message}`);
        }
        displayGif(giphyResponse);
      })
      .catch(function(error) {
        displayErrors(error.message);
      })
  });
});

Let's start by focusing on the most important code. In the process, we'll also explain our new UI functions as well.

src/main.js
WeatherService.getWeather(city)
      .then(function(weatherResponse) {
        if (weatherResponse instanceof Error) {
          throw Error(`OpenWeather API error: ${weatherResponse.message}`);
        }
        const weatherDescription = weatherResponse.weather[0].description;
        displayWeatherDescription(weatherDescription);
        return GiphyService.getGif(weatherDescription);
      })
      .then(function(giphyResponse) {
        if (giphyResponse instanceof Error) {
          throw Error(`Giphy API error: ${giphyResponse.message}`);
        }
        displayGif(giphyResponse);
      })
      .catch(function(error) {
        displayErrors(error.message);
      });

We start by calling our WeatherService static method. Because this returns a promise, we can use Promise.prototype.then() with it.

Error Handling

Next, we've added more robust error handling. If our static method throws an error, control will revert to the catch block in the static method. But we don't want to just throw an error for developers to see. We want our users to see an error message, too. While we could technically just do so in the catch block of our weather service, that's not good separation of code. If there's an error in the API call, our weather service should just return an error. It shouldn't know or care about the UI at all.

So now when we call Promise.prototype.then() in our UI, it will return either the data from the API or an error object. We can check to see if it returned an error with the following conditional:

if (weatherResponse instanceof Error)

As we discussed in Exception Handling in JavaScript, instanceof is actually an operator and it's very helpful for checking to see whether or not something is a certain type of object. In this case, if weatherResponse is an instance of an Error object, we need to throw another error. This one will revert control to the nearest catch block - which actually comes at the end of our chained promise. That catch block will call a displayErrors() function which will display an error for the user.

So here's what's happening when the API call doesn't return a 200 response:

  • In our static method, control switches over to a conditional which then throws an error.
  • The catch block in our static method catches the error, then returns an Error object from the method. This object has a message property that contains information about the error.
  • When the static method is resolved, control switches over to Promise.prototype.then(). The method checks if weatherResponse is an instance of an Error object. It is, so we throw another error which will then switch control to the catch block of our chained promises and pass along the error message.
  • Finally, the catch block will call a displayErrors() function.

If it seems like we are throwing this error all over the place, we are. But that's common in coding - think about a time when you've made an error in a JavaScript application that uses webpack. The application will fail to compile and there will be a stack trace. It's often not just one error, but an entire cascade of an errors that the first one triggers.

In this particular application, there are several advantages to the approach we're taking here. First, we wouldn't want to call displayErrors() in our services. It has to do with our UI and services shouldn't know or care about our UI. On the other hand, it makes complete sense for our service to either return data or throw an error. We could use this service anywhere in any application and handle the error as needed - for instance, if we were also using this service in a backend Node application, we wouldn't want to display an error to a user - instead, we might print the error message to a server error log.

There's another big advantage. The chained promises in main.js are handling multiple API calls. We can use the same catch block to handle errors from either. We just need to have a slightly different error message so the user can see exactly what went wrong. In the case of the Weather API response. the message is OpenWeather API error: ${weatherResponse.message}. In the case of the Giphy response, that message will be Giphy API error: ${giphyResponse.message}. (If you want to see these error messages for yourself when you read the code, the easiest way is to trigger a 403 Unauthorized response by removing the API key or changing it to a bogus value like 1.)

So now we've set up our error handling so that messages related to the UI are in the correct place - and we can use the same catch block for errors related to both API calls, keeping our code DRY.

Successful Calls

Now let's take a look at what will happen if all goes well with our OpenWeather API call:

src/main.js
const weatherDescription = weatherResponse.weather[0].description;
displayWeatherDescription(weatherDescription);
return GiphyService.getGif(weatherDescription);

First we parse the description property from the response data. Then we have another callback - all the displayWeatherDescription() function does is display the weather description to the user. Why do it here instead of later? Well, there's no reason for users to wait until the Giphy API call is resolved for them to get some data. That's a huge advantage of asynchronous code and something you'll see often online - sites like Facebook and Twitter will give you some data now even as they are loading more data to show you later. We don't want to wait for everything to load all at once. Also, if the Giphy API call were to fail, we'd still get data from the OpenWeather API.

Now for the big gotcha that trips students up. Promise.prototype.then() is a method. We know that, right? Well, what's the basic rule about JavaScript methods/functions always returning something or their return will be undefined? That applies to Promise.prototype.then(), too. Because the syntax of chaining promises together looks confusing at first, it can be really easy to forget to add the return. You must return a value from Promise.prototype.then() if you want to chain another promise to it.

In this case, we return our next API call to the Giphy API. If there's an error, our code will go through the same process as it does with the OpenWeather API. If it's successful, there will be another callback to a UI function called displayGif();. We don't need to return anything from this method because there are no further promises chained to it. In fact, this final method just has side effects. A method or function that alters something elsewhere in the code (instead of or in addition to returning a value) is said to have side effects. Side effects are common with functions related to the UI - though when it comes to business logic, they should generally be avoided. Well, we did avoid them in our service logic because we made sure to keep our UI logic separate.

And that's it for the code. The most complex change is the error handling. And while it can seem daunting at first, if you think carefully about keeping code separate and using callbacks to make the code more modular, it's not so bad. We'll run into a lot more trouble (and generally bad code) if we just throw a lot of code inside Promise.prototype.then() without extracting as much as possible into separate functions.

HTML

One last thing: we've updated the HTML as well. There's really nothing to say about it other than the fact that we had to add and remove some HTML tags to account for handling GIFs and no longer handling the temperature:

src/index.html
<html lang="en-US">
<head>
  <title>Weather</title>
</head>
<body>
  <div class="container">
    <h1>Get Weather Conditions From Anywhere!</h1>
    <label for="location">Enter a location:</label>
    <input id="location" type="text">
    <button class="btn-success" id="weatherLocation">Get the weather</button>
    <div class="weather-description"></div>
    <div class="show-gif"></div>
    <div class="show-errors"></div>
  </div>
</body>
</html>

And that's all of our updated code. Even though this lesson is optional and chaining promises isn't required for the independent project, we highly recommend trying to chain promises in your on code - doing so will give you a better understanding of promises, error handling, and other important JavaScript concepts.


Example GitHub Repo for API Project with Chained Promises

Lesson 26 of 29
Last updated April 8, 2021