June 11, 2021
Read Time: 6 mins

Javascript Promises Tips and Tricks

So you hear about all the time about JavaScipt's event loop and how the it's an event driven single threaded programming language. But you don't fully grasp the implications of that statement until you start working with async code like calling an external API with the fetch function.

When you work with asynchronous code you have delve into the world of callbacks or promises.

In my case, I never had any issue understanding callbacks. They are ugly and produce the famous callback hell when you can end up with functions that call functions that call functions that call... You guess it, functions. But they are easy to understand.

With promises I had a little more trouble since the syntax made me think that code that once was asynchronous, it magically became synchronous. And that's not the case. It's still asynchronous, but a little more flatter.

When async await came into de picture, all became more clear once I understood that async code need to be encapsulated in it's own function

So here there are a couple of tricks that I've learned to work with asynchronous code and most of all, how to work with Promises without losing my mind.

TOC

Setup

If you want to follow along, you can start by setting up an example project:

mkdir javascript-async-await-tips
cd $_
npm init -y
npx eslint --init # Answer the questions
npm install --save-dev eslint-plugin-prettier eslint-config-prettier

Change the .eslintrc.json file to enable Prettier.

{
  "env": {
    "commonjs": true,
    "es2021": true,
    "node": true
  },
  "extends": ["eslint:recommended", "plugin:prettier/recommended"],
  "parserOptions": {
    "ecmaVersion": 12
  },
  "rules": {
    "prettier/prettier": ["warn"]
  }
}

And in package.json create a lint command to make things easier to fix.

{
  "...",
  "scripts": {
    "lint": "eslint --fix src/**/*.js"
  }
}

Since I'll be using node, I need the axios package to make requests, since fetch is only available in the browser.

npm install axios --save

Cool, we're ready to start creating code examples.

Creating a basic promise

Let's start with a basic promise. Let's use the Json Placeholder API from Typicode to extract some information.

// src/photos-promise.js
const axios = require("axios")
const photosUrl = "https://jsonplaceholder.typicode.com/photos"

axios({
  url: photosUrl,
  method: "GET",
})
  .then(res => {
    // Here is the main code of your application.
    console.log(res["data"])  })
  .catch(err => {
    console.error(err)
  })

console.log("End of the code")

There are 2 issues with this kind of code:

  • The main code of your application gets buried inside a then statement
  • The execution order can throw you off

Let's execute the script to prove that last statement:

$ node src/photos-promise.js

End of the code[
  {
    albumId: 1,
    id: 1,
    title: 'accusamus beatae ad facilis cum similique qui sunt',
    url: 'https://via.placeholder.com/600/92c952',
    thumbnailUrl: 'https://via.placeholder.com/150/92c952'
  },
  {
    albumId: 1,
    id: 2,
    title: 'reprehenderit est deserunt velit ipsam',
    url: 'https://via.placeholder.com/600/771796',
    thumbnailUrl: 'https://via.placeholder.com/150/771796'
  },
  {
    albumId: 1,
    id: 3,
    title: 'officia porro iure quia iusto qui ipsa ut modi',
    url: 'https://via.placeholder.com/600/24f355',
    thumbnailUrl: 'https://via.placeholder.com/150/24f355'
  },
  ... 4900 more items
]

See how the message End of code comes first? That's asynchronous code works, but it also can make your development experience less enjoyable.

Converting a promise to async await

Using async await helps your code to make more sense since you can crate a part of your code behave like synchronous code. Still, there are a couple of gotchas:

  • You have to enclose the promise code inside a function
  • The function has to be preceded by the async code
  • The part of the code that returns a promise has to be preceded by the await statement
  • The new function is also asynchronous so you might need to use an IIFE function to call it

If we convert the previous promise into async await this is what we'll en up with:

// src/photos-async-await.js
const axios = require("axios")
const photosUrl = "https://jsonplaceholder.typicode.com/photos"

const getPhotos = async () => {
  try {
    const res = await axios({ url: photosUrl, method: "GET" })
    console.log(res["data"])
  } catch (err) {
    console.error(err)
  }
}

getPhotos()
console.log("End of code")

And if we execute it this is the result:

$ node src/photos-async-await.js
End of code
[
  {
    albumId: 1,
    id: 1,
    title: 'accusamus beatae ad facilis cum similique qui sunt',
    url: 'https://via.placeholder.com/600/92c952',
    thumbnailUrl: 'https://via.placeholder.com/150/92c952'
  },
  {
    albumId: 1,
    id: 2,
    title: 'reprehenderit est deserunt velit ipsam',
    url: 'https://via.placeholder.com/600/771796',
    thumbnailUrl: 'https://via.placeholder.com/150/771796'
  },
  ...
]

Notice how we still get the End of code string before the results. That's because getPhotos is asynchronous.

Using a IIFE function for top-level await

To fix the issue of getting the End of code before the API call, we can enclose our main code in an Async Self Executing Function, or IIFE.

// src/photos-async-await.js
const axios = require("axios")
const photosUrl = "https://jsonplaceholder.typicode.com/photos"

const getPhotos = async () => {
  try {
    const res = await axios({ url: photosUrl, method: "GET" })
    console.log(res["data"])
  } catch (err) {
    console.error(err)
  }
}

;(async () => {  await getPhotos()  console.log("End of code")})()

If you test this, you'll see that the End of code gets printed last.

Using Promise.all() and Promise.any()

The Promise object is not completely useless now that Async Await exists. Its still very useful with Promise.all() for example:

// src/photos-promise-all.js
const axios = require("axios");
const photosUrl = "https://jsonplaceholder.typicode.com/photos";

const getPhotos = async () => {
  try {
    const res = await Promise.all([      axios({ url: `${photosUrl}/15`, method: "GET" }),
      axios({ url: `${photosUrl}/25`, method: "GET" }),
      axios({ url: `${photosUrl}/35`, method: "GET" }),
    ]);
    return res.map((item) => item["data"]);  } catch (err) {
    console.error(err);
  }
};

(async () => {
  const photos = await getPhotos();  console.log(photos);
})();

Before I explain, notice how we changed the URL to fetch just one photo by specifying a photo id.

  • In line 7 we use the Promise.all function to make 3 calls to an external API, one after the other.
  • Since Promiese.all returns an array with the results, we used a map to traverse the results and return an array
  • In line 19 we used the returned value which means that async function can have return values, but they have to be used inside an another async function