Javascript Promises, Async and Await 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 where 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.

Just remember that to chain promises, you have to return a promise at the end of each then.

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 async function

That’s why before NodeJS 16 you had to use IIFE functions. But more on that latter.

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
npm install --save-dev eslint prettier
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"
  }
}

Finally, create the .editorconfig file so your editor behaves as similar as prettier wants.

root = true

[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf
; editorconfig-tools is unable to ignore longs strings or urls
max_line_length = off

[*.md]
indent_size = false

Superagent

I’ve talked about how the JavaScript function fetch as a good example of an async function, but I want to work in the terminal with the node command, and NodeJS does not support the fetch function. An excellent replacement for fetch, that works both on the browser as in the server, is superagent package to make requests, since fetch is only available in the browser. Additionally, it work both using callbacks and with promises, so it’s perfect for this article.

To install it, issue the now too familiar npm install command:

npm install superagent --save

Cool, as far as setup goes, we’re done. Let’s code.

The “problem” with asynchronous functions

Let’s start explaining how asynchronous functions work and some of it’s issues. And for that let’s create a simple broken function that “reads” the contents of a file.

// src/read-config.js
const fs = require("fs");
const path = require("path");

function readConfig(filename) {
  const config = path.dirname(__dirname) + `/config/${filename}`;
  let photosUrl = null;

  fs.readFile(`${config}`, "utf8", (err, data) => {
    if (err) {
      throw new Error(`Could not read the file ${config}`);
    }
    photosUrl = data.trim();
  });

  return photosUrl;
}

console.log(readConfig("photos.txt"));

The idea of this function is that it will try to read the contents of a file (the name of the file is the received parameter) placed in the config/ directory and return its contents.

Now, this function has a BIG issue. If we execute it with node this is what we’ll get:

$ node read-config.js

null

The “problem” is that fs.readFile is an asynchronous function, which means that the program execution won’t wait for that function to execute. It will continue to programs flow.

So, when fs.readFile finishes reading the config/photos.txt file, console.logwill already have been executed.

Now, to be fair, this is not a problem, but a great advantage of the language because some tasks, like reading a file, won’t stop the program flow. But it offers a great challenge for developers.

How to work with asynchronous functions

To fix the previous error, we have to change the readConfig function so it uses callbacks to print out the contents of the photos.txt file:

// src/read-config-callback.js
const fs = require("fs");
const path = require("path");

function readConfig(filename, callback, error) {
  const config = path.dirname(__dirname) + `/config/${filename}`;

  fs.readFile(`${config}`, "utf8", (err, data) => {
    if (err) {
      error(err);
    }
    callback(data.trim());
  });
}

readConfig(
  "photos.txt",
  function (contents) {
    console.log(`The config contents are "${contents}"`);
    // Execute additoinal callbacks here
  },
  function (err) {
    console.error(`The configuration file could not be read:`, err);
  }
);

Notice how the function now executes the passed callbacks

To work with fs.readFile we had to make 2 big changes:

And now, the readConfig function won’t return a string with the contents of the photos.txt file. Instead, it will execute functions on success or error.

Also, here we start to see the famous Callback Hell issue, where we have to pass callback function all over the place.

There is actually a site called Callback Hell that explains this problem.

And if we execute the script, we get something like:

$ node src/read-config-callback.js

The config contents are "https://jsonplaceholder.typicode.com/photos"

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:

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 how asynchronous code works, it allows you to do multiple things at once, 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:

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.

You might be wondering why the ; before the IIFE function?. Well, since JavaScript does not require ; at the end of the function, the interpreter might get confused when it sees a ( as the first thing in the line. This is kind of a long discussion, so for now I’ll say that using a ; there is a good practice but not required.

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.