Vincent A Saulys' Blog
Javascript Async is Baffling
Tags: javascript
April 15, 2021

Javascript async is completely baffling to me and I can't get over it.

Let's take a few examples with NodeJS.

First misconception: javascript defaults to sequential use:

const foo = () => {
  setTimeout(() => console.log("very long function finished"),
  10000);  
};

console.log("start");
foo();
console.log("end");

What prints out?

If you look on the internet, you'll often find that people say "javascript is async." This function will, in fact, finish that way:

start
end
very long function finished

However, this trivial example works this way solely because it uses setTimeout. That triggers something outside the event loop.

If we rewrite this to actually do work, it will be syncronous.

const findAllPrimes = (n = 1000000) => {
  let i = 3;
  let allPrimes = [];

  while (i < n) {
    let j = 2;
    while (j < i) {
      if (i % j == 0) {
        break;
      }

      if (i == j + 1) {
        allPrimes.push(i);
      }
      j++;
    }
    i++;
  }
  return allPrimes;
};

console.log("start");
console.log(findAllPrimes());
console.log("end");

This script will hang after the function call:

start
# ... wait a very long time
# [ ... ] return a very long list
end

This is synchronous behavior to my eyes. It's doing one command, stopping, then picking up again.

What people are really referring to with javascript's async behavior comes from how many many functions are built. They use promises or async notation. You then promise chain them:

// technically fetch is only in the browser. 
fetch("https://vincentsaulys.com/crypto-thesis.html").then((res) =>
  console.log(res)
);

Or wrap it in this (rather ugly) anonymous function:

(async () => {
  const thing = await fetch("https://vincentsaulys.com/crypto-thesis.html");
  console.log(thing);
})();

But this behavior -- where some function act absolutely asynchronously and others act exactly as you'd think -- is very baffling to me. This is doubly so with having two synaxes for it: await and .then chaining.

Trying to combine these two is painful even though its far easier to think of things in await as this mimics other programming languages. I've covered this before.

My "favorite" pattern, in addition to the anonymous function above, would have to be promise resolving a lot of things at once.

Let's say we want to fetch several things at once. We could chain them one by one but that takes a long time. Fetching multiple files is usually best asynchronously because each request is independent. Because this code repeats, we could wrap it in a list and map these values out.

const _links = [
  "https://vincentsaulys.com/3dfx-and-unconventional-markets.html",
  "https://vincentsaulys.com/Getting Citrix to Work with Linux.html",
  "https://vincentsaulys.com/always-be-consistent-with-coding-styles.html",
  "https://vincentsaulys.com/click-to-edit-vanilla-js.html",
  "https://vincentsaulys.com/crypto-thesis.html",
  "https://vincentsaulys.com/django-cbvs-guide.html",
  "https://vincentsaulys.com/ePub-as-encapsulations-of-Websites.html",
  "https://vincentsaulys.com/gpt-3-the-universal-algorithm.html",
  "https://vincentsaulys.com/its-a-feature-until-necesary.html",
  "https://vincentsaulys.com/modularization-of-human-teams.html",
  "https://vincentsaulys.com/on-language-learning.html",
  "https://vincentsaulys.com/pages/about-me.html",
  "https://vincentsaulys.com/promises-in-javascript-are-wild.html",
  "https://vincentsaulys.com/software-creates-the-box-you-think-in.html"
];

_links.map((link) => {
  fetch(link).then((res) => console.log(res));
});

This very simple fetch will put them to console. What if we want to return them?

Welcome to weirdness.

const _links = [
  "https://vincentsaulys.com/3dfx-and-unconventional-markets.html",
  "https://vincentsaulys.com/Getting Citrix to Work with Linux.html",
  "https://vincentsaulys.com/always-be-consistent-with-coding-styles.html",
  "https://vincentsaulys.com/click-to-edit-vanilla-js.html",
  "https://vincentsaulys.com/crypto-thesis.html",
  "https://vincentsaulys.com/django-cbvs-guide.html",
  "https://vincentsaulys.com/ePub-as-encapsulations-of-Websites.html",
  "https://vincentsaulys.com/gpt-3-the-universal-algorithm.html",
  "https://vincentsaulys.com/its-a-feature-until-necesary.html",
  "https://vincentsaulys.com/modularization-of-human-teams.html",
  "https://vincentsaulys.com/on-language-learning.html",
  "https://vincentsaulys.com/pages/about-me.html",
  "https://vincentsaulys.com/promises-in-javascript-are-wild.html",
  "https://vincentsaulys.com/software-creates-the-box-you-think-in.html"
];

const things = _links.map((link) => {
  return fetch(link);
});

console.log(things);
# output
[ Promise { "pending" }, Promise { "pending" }, Promise { "pending" },
Promise { "pending" }, Promise { "pending" }, Promise { "pending" },
Promise { "pending" }, Promise { "pending" }, Promise { "pending" },
Promise { "pending" }, … ]

Ah fantastic!

But we could turn this into an async function no? then put an await?

const _links = [
  "https://vincentsaulys.com/3dfx-and-unconventional-markets.html",
  "https://vincentsaulys.com/Getting Citrix to Work with Linux.html",
  "https://vincentsaulys.com/always-be-consistent-with-coding-styles.html",
  "https://vincentsaulys.com/click-to-edit-vanilla-js.html",
  "https://vincentsaulys.com/crypto-thesis.html",
  "https://vincentsaulys.com/django-cbvs-guide.html",
  "https://vincentsaulys.com/ePub-as-encapsulations-of-Websites.html",
  "https://vincentsaulys.com/gpt-3-the-universal-algorithm.html",
  "https://vincentsaulys.com/its-a-feature-until-necesary.html",
  "https://vincentsaulys.com/modularization-of-human-teams.html",
  "https://vincentsaulys.com/on-language-learning.html",
  "https://vincentsaulys.com/pages/about-me.html",
  "https://vincentsaulys.com/promises-in-javascript-are-wild.html",
  "https://vincentsaulys.com/software-creates-the-box-you-think-in.html"
];

const things = _links.map(async (link) => {
  const toRet = await fetch(link);
  return toRet;
});

console.log(things);
[ Promise { "pending" }, Promise { "pending" }, Promise { "pending" },
Promise { "pending" }, Promise { "pending" }, Promise { "pending" },
Promise { "pending" }, Promise { "pending" }, Promise { "pending" },
Promise { "pending" }, … ]

No improvement.

What's going on?

All these functions only await within that code block. If we put console logs after the await, they will behave as we expect. But the function is wrapper -- has to be wrapped to call await -- inside of an async function. That function returns a promise which will eventually complete.

Promise { <state>: "fulfilled", <value>: Response }

The pattern is to wrap this with a big 'ol Promise.all and then chain that out to console log. Then we'd have to have a function complete it.

const _links = [ ... ]

// shortened the code
const promises = Promise.all(_links.map((link) => fetch(link)));
console.log(promises);
Promise { <state>: "pending" }

Gurr, stil not getting there.

If we let this hang in the console, we can call it later and see that the values return.

Promise { "fulfilled" }
<state>: "fulfilled"
<value>: (14) [...]
0: Response { type: "basic", url: "https://vincentsaulys.com/3dfx-and-unconventional-markets.html", redirected: false, … }
1: Response { type: "basic", url: "https://vincentsaulys.com/Getting%20Citrix%20to%20Work%20with%20Linux.html", redirected: false, … }
2: Response { type: "basic", url: "https://vincentsaulys.com/always-be-consistent-with-coding-styles.html", redirected: false, … }
3: Response { type: "basic", url: "https://vincentsaulys.com/click-to-edit-vanilla-js.html", redirected: false, … }
4: Response { type: "basic", url: "https://vincentsaulys.com/crypto-thesis.html", redirected: false, … }
5: Response { type: "basic", url: "https://vincentsaulys.com/django-cbvs-guide.html", redirected: false, … }
6: Response { type: "basic", url: "https://vincentsaulys.com/ePub-as-encapsulations-of-Websites.html", redirected: false, … }
7: Response { type: "basic", url: "https://vincentsaulys.com/gpt-3-the-universal-algorithm.html", redirected: false, … }
8: Response { type: "basic", url: "https://vincentsaulys.com/its-a-feature-until-necesary.html", redirected: false, … }
9: Response { type: "basic", url: "https://vincentsaulys.com/modularization-of-human-teams.html", redirected: false, … }
10: Response { type: "basic", url: "https://vincentsaulys.com/on-language-learning.html", redirected: false, … }
11: Response { type: "basic", url: "https://vincentsaulys.com/pages/about-me.html", redirected: false, … }
12: Response { type: "basic", url: "https://vincentsaulys.com/promises-in-javascript-are-wild.html", redirected: false, … }
13: Response { type: "basic", url: "https://vincentsaulys.com/software-creates-the-box-you-think-in.html", redirected: false, … }

Ok so how do we solve this?

In practice, you have this pipe out in a further .then(...).catch(...) chain. This very common in ExpressJS.

What you could do in a pinch is wrap the thing in anonymous function like above:

(async () => {
  const promises = await Promise.all(_links.map((link) => fetch(link)));
  console.log(promises);
})();

This will work as we expect.

Array(14) [ Response, Response, Response, Response, Response, Response, Response, Response, Response, Response, … ]

But while everything inside that function will behave we expect, the function itself is async so any statements outside of it will not run. In fact, if we call await in the node shell we'll get an error.

> const thing = await 1+2
const thing = await 1+2
              ^^^^^

Uncaught:
SyntaxError: await is only valid in async functions and the top level bodies of modules

This is very counter-intuitive and, I think, rather baffling.

Despite all of this, javascript lives on. Once a library is written, no matter how painfully, its got async "out of the box" so you can combine it fairly easily. It's the writing of those libraries or stepping out of that box where things get painful. I find this very intriguing.

"There are two types of programs: the one people complain and the other they never use"

-- Bjarne Stroustrup, inventor of C++

Share on...