News & Views

What are JavaScript Promises?

Whether you are writing code for the browser or the backend, programming in JavaScript means writing chunks of code that could run at any time in the future. For example, imagine a web page that displays trains schedules for different stations. When a user chooses one of the stations, the data for that station is retrieved from the network, and then that data is shown to the user.

While the browser waits to receive the train schedule data, it continues to respond to clicks, taps, and swipes (which could trigger other bits of JavaScript). Once the data arrives, the browser runs the JavaScript code that processes the data and draws it to the page. Because there is no way to predict how long it takes to retrieve the data, we say that this code is asynchronous. That is, it will run eventually, but other code may run in the meantime.

When learning JavaScript, newcomers must master a number of asynchronous programming techniques. One of these is the use of Promises.

JavaScript, the overworked chef

Computer programs are a lot like food recipes. You might have recipes that refer to other recipes. Pretend that you’re making a turkey sandwich using the following recipe:

  • take two pieces of bread
  • lay three turkey slices on one slice of bread
  • put lettuce, tomato, and cheese on top of the turkey
  • spread cranberry mayo on other slice of bread (see receipe for cranberry mayo)
  • combine, cut in half, and serve

As you’re making your sandwich, you realize “Ah. I must now make the cranberry mayo.” So you pause the sandwich recipe, run the mayo recipe, and then resume the sandwich recipe. But what if you don’t have any cranberries on hand? In real life, you drive to the store, buy some cranberries, then finish making your sandwich. Many programming languages work this way – you can stop everything until you’ve retrieved additional resources.

But in the realm of JavaScript, you’re not just making a single sandwich at home. You’re the only chef in a very busy restaurant, making a lot of other dishes, and you can’t stop all of them to drive to the store. So, you send someone to the store, and whenever they get back you’ll finish that sandiwch. In the meantime, you’re free to keep on cooking.

All aboard

(Note: the following code uses jQuery and arrow functions. In case you’re not familiar with these, feel free to jump down to the Resources section at the bottom of this post.)

If you already have experience with JavaScript, you may have written code like this:

// Get the train schedule data
$.get('/schedule', schedule  =>  
// Convert that raw schedule data into DOM Elements.
// (Just pretend that we've created a `format` function that does that.)
let formattedSchedule  =  format(schedule) ;

// Draw those elements to the page.
$( '#output').append(formattedSchedule) ;
}) ;

We have three distinct steps:

  1. Get the schedule data.
  2. Turn the schedule data into DOM elements.
  3. Draw the DOM elements to the screen.

The first step is asynchronous; we have no idea how long it will take to retrieve that information. For an asynchronous step, we supply two pieces of information: 1) the server path where the data resides and 2) a function that describes what to do with the data once it arrives.

This second part, the function that runs after the data arrives, is a callback function. (Like a “callback” number left in a voicemail.)

When things go wrong

Our example code assumes that everything will go a-ok: the schedule data will arrive just fine. But what if this isn’t the case?

To handle errors in asynchronous code, you supply additional callback functions:

// Get the train schedule data
$.get( '/schedule' , schedule  =>  {
// Convert that raw schedule data into DOM Elements.
// (Just pretend that we've created a `format` function that does that.)
let formattedSchedule = format(schedule) ;

// Draw those elements to the page.
$('#output').append(formattedSchedule) ;
}, err =>{
// Handle error for a single station's schedule data.
console.log('Could not retrieve schedule');
});

This code is fine and does what is intended. But let’s be honest - it’s a bit of a tangle.

Ideally, code is organized into small, logical pieces that have a dedicated job. Each piece should be easy to swap for an equivalent piece. Sadly, our code is currently one giant blob.

Presto change-o

To get ready for Promises, let’s break our code up into functions.

const handleScheduleError = err  =>{
// Handle error for a single station's schedule data.
console.log('Could not retrieve schedule') ;
};
const drawToScreen = formattedSchedule => $('#output').append(formattedSchedule);
const formatSchedule = scheduleData => format(scheduleData);

// Get the train schedule data
$.get('/schedule', schedule =>{
let formattedSchedule =  formatSchedule(schedule) ;
drawToScreen(formattedSchedule);
}, handleScheduleError);

This is a good step, but Promises could make this short example even better.

And now, Promises!

A Promise is a JavaScript object that represents an eventual result. That result is either the value you were seeking, or the error that occured while you were looking.

A trip to the dry cleaners

Imagine taking your lucky shirt to the dry cleaners, as you’ve smeared some barbecue sauce on it. Naturally, it’s going to take time to clean the shirt. You’re not going to wait around at the dry cleaners all day for your shirt. So, the attendant hands you a ticket.

Except, this isn’t just a regular paper ticket. It’s a magic ticket that will either turn green when your shirt is ready, or it will turn red (signaling that your shirt got damaged or lost).

A JavaScript Promise is like our magic ticket. But it doesn’t change colors. Instead, you tell it what functions to run if the asynchronous operation is a success, and what functions to run if there’s an error.

Specifically, a Promise is an object with a .then method and a .catch method. You register “success” functions using .then and “error” functions using .catch.

Back on the train

Luckily, jQuery’s Ajax methods (such as $.get) already return Promises.

When calling $.get, let’s remove the success and error callbacks, intead registering them with .then and .catch.

  
const handleScheduleError = err =>  {
// Handle error for a single station's schedule data.
console.log( 'Could not retrieve schedule') ;
};

const drawToScreen = formattedSchedule => $('#output').append(formattedSchedule);

const formatSchedule = scheduleData => format(scheduleData);

// Get the train schedule data
let retrieveSchedule = $.get('/schedule');
retrieveSchedule.then(formatSchedule);
retrieveSchedule.then(drawToScreen);
retrieveSchedule.catch(handleScheduleError);

After introducing the retrieveSchedule variable, we can use it to register success and error handlers. But it is much more idiomatic to omit the variable, and simply chain our .then and .catch function calls together:

$.get('/schedule')
.then(formatSchedule)
.then(drawToScreen)
.catch(handleScheduleError);

Now our code almost reads like plain English! Here’s how it works: $.get will make the Ajax request. If it is successful, the first function registered with .then (formatSchedule) will be passsed the data retrieved from the server. Whatever value is returned by formatSchedule is passed to the next function registered with .then (drawToScreen) . If there is an exception thrown by the Ajax request or one of those .then callbacks, the function registered with .catch will be called.

I like to think of these as “Promise chains” where each link in the chain feeds data to the next. When you have multiple links in the chain, intermediary links (such as formatSchedule) must return a value. Otherwise, the next link (in this case drawToScreen) would not receive any arguments.

Adding a link to the chain

Knowing that a .then callback will receive an argument (returned by the previous link) and must return a value (which will be passed to the next link), let’s write another function for our Promise chain.

Perhaps it would be useful to visually highlight a particular schedule item when it is clicked. We can do this by adding a class name via event handler.

const handleScheduleError = err => {
// Handle error for a single station's schedule data.
console.log('Could not retrieve schedule');
};
const drawToScreen = formattedSchedule => $('#output').append(formattedSchedule);

const formatSchedule = scheduleData => format(scheduleData);

const addHighlighting = formattedSchedule  => {
// map will transform our `formattedSchedule` array
// into an array of elements that have click listeners.
return  formattedSchedule.map(scheduleItem => {
scheduleItem.addEventListener('click', e => {
e.preventDefault();
e.target.classList.add('highlight');
})
return scheduleItem;
})
};

// Get the train schedule data
$.get('/schedule')
.then(formatSchedule)
.then(addHighlighting)
.then(drawToScreen)
.catch(handleScheduleError);

In our new addHighlighting function, it is imperative that we put the return keyword in front of formattedSchedule.map. Since .map produces our new and improved array of DOM elements, we want to make sure and hand it to the next function in our .then chain.

Summary

Promises improve on callbacks, formalizing how and when callbacks should run. They improve the readability and maintainability of your asynchronous code. You now know that callbacks can be registered using .then and .catch. Your .then callbacks can be chained together, as long as they are prepared to receive a value from the previous link and will return a value to the following link.

Resources

Chris Aquino Chris Aquino Immersive Instructor