DEV Community

loading...

Converting to Asynchronous Code Using IIFEs

thomasstep profile image Thomas Step Originally published at thomasstep.com Updated on ・4 min read

I have not seen too much written about IIFEs in Javascript, but I think that they are a super useful tool for converting chunks of synchronous code to excute asyncronously.

In a nutshell, an IIFE is just a function that you execute at the same time you define it. The following is an example of a Hello World function running as a normal function and as an IIFE.

// Normal function
function helloWorld() {
  console.log('Hello World from normal function!');
}

helloWorld();

// IIFE
(() => {
  console.log('Hello World from IIFE!');
})();
Enter fullscreen mode Exit fullscreen mode

Go ahead and copy paste that into a file and run it, I'll wait. You should see

Hello World from normal function!
Hello World from IIFE!
Enter fullscreen mode Exit fullscreen mode

When I first learned about this syntax I did not think too much about it. If you are simply wrapping static statements, why not just write out the statements? If there is some sort of parameter that you want to pass as a variable, why not just make it a function and call it normally? I still have not personally come across a use case for IIFEs in those contexts; however, I have come across use cases for using it to convert synchronous code to run asynchronously.

Before I go any further, I am assuming that you know about async code in JavaScript, specifically Node, and Promise.all. The whole topic of async code is a bigger concept in and of itself, so if you do not know much about it, then I suggest learning that first. If you already know about how async programming works, then Promise.all will not be hard to understand. It is simply a way to block execution until the Promises you provide the function resolve. An example looks like this.

let i = 0;

async function echo(string) {
  console.log(string);
  i += 1;
  return i;
}

Promise.all([
  echo('first'),
  echo('second')
]).then((nums) => {
  console.log(nums);
});
Enter fullscreen mode Exit fullscreen mode

You can run this, but don't expect anything profound to log.
This is what I got

first
second
[ 1, 2 ]
Enter fullscreen mode Exit fullscreen mode

All I am trying to illustrate is that Promise.all takes in an iterable object and resolves an array of all the resolved Promises you gave it. Easy. Next comes the fun part.

Let's say I have some synchronous code that takes "forever" to run.

function sleep(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function func1() {
  await sleep(1000);
  return 1;
}

async function func2() {
  await sleep(2000);
  return 2;
}

async function func3() {
  await sleep(3000);
  return 3;
}

async function func4() {
  await sleep(4000);
  return 4;
}

async function main() {
  const startTime = process.hrtime();

  const num1 = await func1();
  const num2 = await func2();
  const num3 = await func3();
  const num4 = await func4();

  const [endS, endNs] = process.hrtime(startTime);
  const endTime = endNs / 10 ** 6 + endS * 1000;
  console.log(`${endTime} ms`);
  console.log(`Result: ${num1 + num2 + num3 + num4}`);
}

main();
Enter fullscreen mode Exit fullscreen mode

Here I'm using functions, but let's pretend that each function represents statements that reach out to DBs and other APIs and take a while to resolve. For fun I'll say that func1 and func2 reach out to a REST API, func3 reaches out to a GraphQL API, func4 reaches out to a DB. We'll go ahead and say that these functions each represent 20 lines of code or so that connect to the various endpoints and get the correct data because using IIFEs looks a lot better that way.

Now transitioning the previous code snippet to IIFEs we have something that looks like this.

function sleep(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function func1() {
  await sleep(1000);
  return 1;
}

async function func2() {
  await sleep(2000);
  return 2;
}

async function func3() {
  await sleep(3000);
  return 3;
}

async function func4() {
  await sleep(4000);
  return 4;
}

async function main() {
  const startTime = process.hrtime();
  Promise.all([
    (async () => {
      const num1 = await func1();
      const num2 = await func2();
      return num1 + num2;
    })(),
    (async () => {
      const num3 = await func3();
      return num3;
    })(),
    (async () => {
      const num4 = await func4();
      return num4;
    })(),
  ]).then(([num1plus2, num3, num4]) => {
    const [endS, endNs] = process.hrtime(startTime);
    const endTime = endNs / 10 ** 6 + endS * 1000;
    console.log(`${endTime} ms`);
    console.log(`Result: ${num1plus2 + num3 + num4}`);
  });
}

main();
Enter fullscreen mode Exit fullscreen mode

Again, try to think of each function that I am calling in the IIFEs as being multiple statements long and not simply a function. I am trying to highlight what IIFEs are, not what goes in them. Also, please note that the IIFE that I just wrote is unlike the original Hello World one at the top in that this most recent one was async. With that disclaimer, I hope you ran both of the last two snippets. If you decided to not run those snippets and instead are just reading along, let me share with you what I got.

# Awaiting run
10002.1091 ms
Result: 10

# IIFE run
4001.5615 ms
Result: 10
Enter fullscreen mode Exit fullscreen mode

The time it takes to get through all of that logic and communication goes from ~10 seconds down to ~4 seconds (or the longest set Timeout). Remember, I am trying to think of it as reaching out to two different APIs and a DB which took me a total of 10 seconds before the IIFE conversion. This gets more and more powerful once you start writing code in chunks that process data independently of other chunks, converting those independent chunks into IIFEs, and then running those IIFEs in parallel. If you can unwind those independent pieces and put the dependent ones into IIFEs, you can significantly speed up your processing time. I can see this being incredibly useful to applications that crunch a ton of data (data science?) or reach out to multiple different services at once before doing anything with the returned data. I have personally used it with success on code that reaches out to multiple APIs at once and then acts on that data once all of the resources have returned. I have also used it with success while looping through some large arrays with map, reduce, forEach, etc.

Have you ever used this type of approach? Are there any potential downfalls of doing something like this? Did you know about IIFEs and have you used them before?

I feel like I see a lack of people writing about using them, but I think they are fun and definitely beneficial.

Discussion (9)

Collapse
joaozitopolo profile image
Joao Polo

Sorry, but... You should to use promise.all in the first approach also, to can compare with the second approach:

//const num1 = await func1();
//const num2 = await func2();
//const num3 = await func3();
//const num4 = await func4();
Promise.all([func1(), func2(), func3(), func4()]).then(([num1, num2, num3, num4]) => ...)

and then, you'll have the same 4 seconds to run.

Awesome idea about small chunks with IIFE.

Collapse
garretharp profile image
Garret

I dont understand at all what this IIFE has to do with reducing the time at all. Its the Promise.all[] that makes it shorter because you arent waiting to go to the next promise.

Am I missing something?

Collapse
thomasstep profile image
Thomas Step Author

You are correct that Promise.all is the main driver in reducing time. I'm simply trying to illustrate how you could use IIFEs together with Promise.all. Another perfectly valid way to speed up execution would be to create normal async functions and use the returned Promises from those functions in Promise.all. This method just short cuts that and allows you to execute the code using an IIFE while still awaiting it.

Simply an alternative to traditional refactoring using functions. Also I don't see much about IIFEs and wanted to write about them. I'm sorry if the title was misleading.

Collapse
pierreneter profile image
Pierre Neter

You can use Top-level await, it has been supported by nodejs 14.3.0 with --experimental-top-level-await flag, to avoid use IIAFEs.
Read more about that: github.com/tc39/proposal-top-level... the limitations on IIAFEs section.

More:
v8.dev/features/top-level-await
bugs.chromium.org/p/v8/issues/deta...

Collapse
omakoleg profile image
Oleg • Edited

I am confused.
This is sequential execution:

const num1 = await func1();
const num2 = await func2();
const num3 = await func3();
const num4 = await func4();

And this is parallel:

Promise.all([
(async () => {
...
})(),
(async () => {
...
})(),
(async () => {
...
})(),
]).then

Parallel would be always faster.

Collapse
chico1992 profile image
chico1992

Why go through all the trouble of writing those IIFE’s instead of simply calling func3?

Collapse
thomasstep profile image
Thomas Step Author

Good question. I tried to clarify that in the post, but obviously did not do a great job.

Again, try to think of each function that I am calling in the IIFEs as being multiple statements long and not simply a function.

The code snippets were more just to illustrate making async IIFEs work with Promise.all, not necessary the purpose of those IIFEs that I wrote. Otherwise you're totally right, you might as well just call the function. If you can imagine taking 20 lines of statements or calculations and replacing func3 with those, then it would make more sense. I just didn't want to write out those 20 lines 😂

Collapse
chico1992 profile image
chico1992

To be honest I still don’t get it, if i understand you correctly I should imagine replacing the body of every IIFE with like 20 lines of code
That would amount to 60-80 lines of code inside the Promise.all
Maybe you have a more concrete example to explain

Collapse
nicolasini profile image
Nico S___

Thats pretty neat. Thanks for sharing.

Forem Open with the Forem app