Finding the Perfect Promise — A JavaScript Journey

Dre May
8 min readMay 3, 2019

--

As I started to write this article, it began as a technical blurb on the merits of using the JavaScript feature of async / await. This concise blurb naturally evolved in my head to a masterpiece of technical know how on all of the possibilities of creating callback mechanisms within JavaScript. That grandeur quickly faded however, as I conceded that these articles must already exist. And with a few searches, I confirmed that yes, literally dozens, if not hundreds of good articles exist on the merits of async await vs. raw promises. Throw in a healthy dose of RxJS observables and a custom written JavaScript event bus, and there is a great recipe for technology overdose.

As I shifted gears to something more down to earth, I realized that the true value was showing my journey from one implementation to the next, the reasons for continued refactoring, and what I learned along the way. Oh, and yes, I will show callbacks, promises, and async / await.

Getting There

My task was relatively straight forward — write a minimal demo script for a new API being developed. The purpose was to create something that could demonstrate concepts to another team with minimal code. Instead of taking the time to write a full-fledged Java implementation, the wise choice for me was to create a short JavaScript sample with Node.js as the runtime.

This script was not meant for production, nor meant to be particularly elegant, performant, or clean — simply as a demo. Powerpoint on steroids. But as I quickly realized, no code can ever escape the lens of self scrutiny.

Rockets Away

The code for my organization is proprietary, so in order to shed light on my journey, I needed to take a contrived path to illustrate my refactoring adventure. Todd Motto has a wonderful list of APIs that are publicly available, and I happened to choose the SpaceX API as an interesting way to re-create my experience. Todd Motto’s API list can be found here:

https://github.com/toddmotto/public-apis

And the SpaceX API I chose for demonstration purposes is located here:

https://github.com/r-spacex/SpaceX-API

Getting Started

If you would like to follow along, some of the pre-requisites are:

  • Install the latest version of npm and Node.js. (I am currently running Node.js version 10.15.1)
  • Within a new directory start off with npm init.
  • Install two packages: axios and lodash.
npm init
npm install axios --save
npm install lodash --save

The Mission

The goal of my demo script is to first call an API from SpaceX to retrieve launches for a previous year, and then find one with mission data. Once that is found, simply print out some information about the mission. This will involve calling two separate APIs from the SpaceX catalog. And this is where the fly enters the ointment of clean code.

Anytime you need to chain multiple asynchronous calls, the invitation goes out to your messiest friends for a party.

Draft One

This is where my first mistake entered the picture. The idea that any code should not be given its fair share of pre-thought before typing is a trap waiting to happen. The temptation to blast away at the keyboard — especially within a scripting environment, is very high. In fact, it is almost encouraged! If a new language doesn’t offer a REPL (Read-eval-print-loop), then it simply isn’t cool.

So here is a version of my first round of code.

Initial Bits

Let’s get started by setting up the two libraries used in my demo:

  • axios (for making HTTP requests)
  • lodash (for some utilities to make the code a little cleaner).
const axios = require('axios');
const _ = require('lodash');

The First API

The first API call to retrieve space launches within a particular year was created below.

function getSpaceDataForYear(year, processResultsFunc) {
axios.get('https://api.spacexdata.com/v3/launches/past', {
params: { launch_year: year} })
.then( (results) => processResultsFunc(results) )
.catch( (err) => console.log (err) );
}

And the calling block starts here.

getSpaceDataForYear("2018", processLaunchDataForYearResult);

At this point, everything seems fine as I frantically type to get my demo done, but as you will see, signs of wear and tear are already appearing. The need to send in a callback function was necessary, as axios calls return a JavaScript promise object. Remember, the temptress of immediate WET (We Enjoy Typing) was fully at play when I embarked on creating this demo.

So we must continue! Enter the next chain of calls, with the definition of processLaunchDataForYearResult.

function processLaunchDataForYearResult(launchResult) {
let flightWithMission = findFlightWithAMission(launchResult);
outputMissionDataSummary(flightWithMission);
getMissionDetails(flightWithMission, processMissionDetailsResult);
}

We are now a second layer deep on what should be a straightforward demo, and it will get worse. As you can see, the next API call is contained within getMissionDetails, and that method will also need a callback function (processMissionDetailsResult).

The function getMissionDetails is outlined below, which is very similar to the function that contained the first API call. Essentially call our callback function in the promise completion of the axios call.

function getMissionDetails(flight, processResultsFunc){
axios.get(
`https://api.spacexdata.com/v3/missions/${flight.missionId}`)
.then( (results) => processResultsFunc(results) )
.catch( (err) => console.log (err) );
}

The rest of the code is located below, which is provided in case you want to re-create the entire working draft one sample.

function findFlightWithAMission(yearMissionData) {
var flightWithMission = null;
for (let value of yearMissionData.data ) {
let flightNumber = value.flight_number;
let missionId = _.get(value, 'mission_id[0]');
let rocketName = _.get(value, 'rocket.rocket_name');
if ( flightNumber && missionId && rocketName ) {
return {
flightNumber: flightNumber,
missionId: missionId,
rocketName: rocketName
};
}
}
}
function outputMissionDataSummary(flight) {
console.log(`Flight number: ${flight.flightNumber}`);
console.log(`Mission ID: ${flight.missionId}`);
console.log(`Rocket name: ${flight.rocketName}`);
}
function processMissionDetailsResult(missionResult) {
outputMissionDetail(missionResult);
}
function outputMissionDetail(missionResult) {
console.log(`Mission Name: ${missionResult.data.mission_name}`);
console.log(`Mission ID: ${missionResult.data.description}`);
}

Round One Evaluation

Round one looked something like this guy pictured here. A hodgepodge chain of parts put together, and not necessarily pretty.

If someone wanted to see the overall flow of the set of APIs, the reader would need to jump from a top level function, to another function, and then to another. If the purpose of the demo was to show the flow of API calls in a concise fashion, the demo failed. After looking at the callback chain, I knew it was time for a refactor.

And it appears that WET (We Enjoy Typing) is a self-fulfilling prophecy, as it requires more refactoring and typing!

Stage Two

Stage two involved a refactor that would allow the consumer of the demo code to observe the potential sequence of API calls without muddling through a myriad of JavaScript functions. The top level refactor is listed below.

getSpaceDataForYear ("2018")
.then( findFlightWithAMission )
.then( outputMissionDataSummary )
.then( getMissionDetails )
.then( outputMissionDetail )
.catch ( (error) => { console.log(error); });

What allowed this concise chain? A heavy dose of Promises everywhere! I changed every method to return a Promise object. In hindsight, maybe this went overboard? Don’t fret however, there is another refactor after this. At least the callbacks were gone.

Here is the code for getMissionDetails, it was reduced to a single line:

function getMissionDetails(flight) {
return axios.get(`https://api.spacexdata.com/v3/missions/${flight.missionI
}`);
}

However, other methods added a Promise wrapper:

function findFlightWithAMission(yearMissionData) {
return new Promise( (resolve, reject) => {
var flightWithMission = null;
for (let value of yearMissionData.data ) {

Here is the rest of the code if you would like to follow along, in order to see all of the changes required to achieve the above conciseness.

function outputMissionDetail(missionResult) {
return new Promise( (resolve, reject) => {
console.log(`Mission Name: ${missionResult.data.mission_name}`);
console.log(`Mission ID: ${missionResult.data.description}`);
resolve(missionResult);
});
}
function outputMissionDataSummary(flight) {
return new Promise( (resolve, reject) => {
console.log(`Flight number: ${flight.flightNumber}`);
console.log(`Mission ID: ${flight.missionId}`);
console.log(`Rocket name: ${flight.rocketName}`);
resolve(flight);
});
}
function findFlightWithAMission(yearMissionData) {
return new Promise( (resolve, reject) => {
var flightWithMission = null;
for (let value of yearMissionData.data ) {
let flightNumber = value.flight_number;
let missionId = _.get(value, 'mission_id[0]');
let rocketName = _.get(value, 'rocket.rocket_name');
if ( flightNumber && missionId && rocketName ) {
flightWithMission = {
flightNumber: flightNumber,
missionId: missionId,
rocketName: rocketName
};
break;
}
}
resolve(flightWithMission);
});
}
function getSpaceDataForYear(year) {
return axios.get('https://api.spacexdata.com/v3/launches/past', {
params: {
launch_year: year
}
});
}
function getMissionDetails(flight) {
return
axios.get(
`https://api.spacexdata.com/v3/missions/${flight.missionId }`
);
}

Stage Two Evaluation

I have two major issues with the second round of WET (have you noticed yet that frantic code typing is not healthy coding?):

  • The main API demo code block is concise; however, it isn’t necessarily clear how data is being passed from one step to the next. Perhaps for some it may be fine; however, it still didn’t look quite right to me.
  • In order to achieve the conciseness I modified every method to return a Promise. This seemed like a mistake as well.

Stage Three

I was on a roll at this point in my re-acquaintance with the variants of JavaScript promises, so I thought I should continue. After all, I had already doubled the expected time I thought I would spend on this task, I may as well make it triple the time.

This time, I would pull out the final stop, and utilize the JavaScript functionality of async / await.

Here is the main code block after the refactor:

(async () => {
let flightData = await getSpaceDataForYear("2018");
let flightWithMission = findFlightWithAMission(flightData);
outputMissionDataSummary(flightWithMission);
let missionDetails = await getMissionDetails(flightWithMission);
outputMissionDetail(missionDetails);
})();

The await keyword essentially erases the callback, erases the promise, and states:

“wait for this to complete, and give me the return data.”

Technically, the promise is not really erased; however, the goal is to give that appearance to your code.

The necessary syntax on the function itself is to add the async keyword to the beginning, as illustrated below.

async function getSpaceDataForYear(year) {
return axios.get('https://api.spacexdata.com/v3/launches/past', {
params: {
launch_year: year
}
});
}

There is one last little important thing to note, which of course adds one last little wart to the main code flow, which is that await is only valid within an async function.

In order to work around this, I wrapped the main flow inside an IIFE (Immediately Invoked Function Expression) with the async keyword. This was a small price I believe, as now asynchronous methods are clearly noted, and the code is more readable.

Summary

I hope you have enjoyed the walkthrough, the sample usage of the SpaceX API, and the various examples of how your code may look with different callback / promise mechanisms.

Some of my key takeaways from this exercise are:

  • Always plan ahead. Unless you have the goal of constant refactoring for the point of refactoring itself (WET principle), then measuring twice, cutting once can save some time.
  • It never hurts to research. Before embarking on a coding exercise, it never hurts to read up on the options for implementation. Even if it is for a non-production scenario, knowledge is power.

Final Code

Here is the final source code for my mock exercise. This may be my motivation to write that automated bot to check for the next space flight and trip to Florida.

const axios = require('axios');
const _ = require('lodash');
(async () => {
let flightData = await getSpaceDataForYear("2018");
let flightWithMission = findFlightWithAMission(flightData);
outputMissionDataSummary(flightWithMission);
let missionDetails = await getMissionDetails(flightWithMission);
outputMissionDetail(missionDetails);
})();
function outputMissionDetail(missionResult) {
console.log(`Mission Name: ${missionResult.data.mission_name}`);
console.log(`Mission ID: ${missionResult.data.description}`);
}
function outputMissionDataSummary(flight) {
console.log(`Flight number: ${flight.flightNumber}`);
console.log(`Mission ID: ${flight.missionId}`);
console.log(`Rocket name: ${flight.rocketName}`);
}
function findFlightWithAMission(yearMissionData) {
var flightWithMission = null;
for (let value of yearMissionData.data ) {
let flightNumber = value.flight_number;
let missionId = _.get(value, 'mission_id[0]');
let rocketName = _.get(value, 'rocket.rocket_name');
if ( flightNumber && missionId && rocketName ) {
return {
flightNumber: flightNumber,
missionId: missionId,
rocketName: rocketName
};
}
}
}
async function getSpaceDataForYear(year) {
return axios.get('https://api.spacexdata.com/v3/launches/past', {
params: {
launch_year: year
}
});
}
async function getMissionDetails(flight) {
return
axios.get(`https://api.spacexdata.com/v3/missions/${flight.missionI
}`);
}

--

--

Dre May
Dre May

Written by Dre May

I love writing about all technology including JavaScript, Java, Swift, and Kotlin.

No responses yet