Understanding the Event Loop and Async Patterns in JavaScript and Node.js

Understanding the Event Loop and Async Patterns in JavaScript and Node.js

Β·

6 min read

If you're a developer, you've probably heard of the event loop and async patterns in JavaScript and Node.js. But what exactly are they, and how do they work?

In this blog post, we'll dive into the event loop and explore different patterns for handling asynchronous code in a non-blocking way.

By the end of this post, you should have a better understanding of how the event loop works and how to use promises, async functions, and native options to write efficient and performant code.

What is an Event Loop?

The event loop is a programming construct that enables non-blocking I/O operations in a program. It is an infinite loop that waits for events or tasks to be added to a queue and then processes them one by one.

Here is a simple example of an event loop in JavaScript:

const eventLoop = () => {
  console.log('start');

  setTimeout(() => {
    console.log('timeout');
  }, 0);

  console.log('end');
};

eventLoop();

Output:

start
end
timeout

In this example, the event loop starts by logging "start" to the console. Then, it adds a timeout to the queue with a delay of 0 milliseconds. The event loop continues to execute the rest of the code and logs "end" to the console. Finally, it processes the timeout event and logs "timeout" to the console.

Process

Here is a set of steps that provide an explanation of the event loop:

  1. The event loop waits for events or tasks to be added to the queue.

  2. When an event or task is added to the queue, the event loop processes it.

  3. If the event or task requires blocking I/O, it is delegated to a worker thread.

  4. The event loop continues to process events or tasks in the queue until it is empty.

Examples

Here are some more code examples that demonstrate the event loop in action:

Example 1:

const eventLoop = () => {
  console.log('start');

  setTimeout(() => {
    console.log('timeout 1');
    setTimeout(() => {
      console.log('timeout 2');
    }, 0);
  }, 0);

  console.log('end');
};

eventLoop();

Output:

start
end
timeout 1
timeout 2

In this example, there are two timeout events added to the queue with a delay of 0 milliseconds. The event loop processes them in the order they were added to the queue.

Example 2:

const eventLoop = () => {
  console.log('start');

  setTimeout(() => {
    console.log('timeout 1');
  }, 1000);

  setTimeout(() => {
    console.log('timeout 2');
  }, 500);

  console.log('end');
};

eventLoop();

Output:

start
end
timeout 2
timeout 1

In this example, there are two timeout events added to the queue with different delays. The event loop processes them in the order of their respective delays.

Async Patterns - Blocking Code

Blocking code is a type of code that stops the execution of a program until it finishes running. This can cause performance issues, especially in event-driven programs that rely on the event loop.

Here is an example of blocking code in JavaScript:

console.log('start');

const result = slowFunction();
console.log(result);

console.log('end');

Output:

start
result of slow function
end

In this example, the program blocks until the slowFunction() complete execution. This can cause delays in the event loop and result in a poor user experience.

Async Patterns - Setup Promises

Promises are a pattern that can be used to handle asynchronous code in a more organized and efficient way. A Promise represents the result of an asynchronous operation, which can either be resolved (successful) or rejected (failed).

Here is an example of using a Promise in JavaScript:

Copy codeconst slowFunctionPromise = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('result of slow function');
    }, 1000);
  });
};

console.log('start');

slowFunctionPromise()
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.error(error);
  });

console.log('end');

Output:

start
end
result of slow function

In this example, the slowFunctionPromise() returns a Promise that is resolved with the result of the slow function after a delay of 1000 milliseconds. The program does not block while waiting for the Promise to resolve, and the event loop can continue to process other events or tasks in the meantime.

Async Patterns - Refactor To Async

Async functions are a pattern that can be used to write asynchronous code in a synchronous-like style. An async function returns a Promise that is resolved with the return value of the function.

Here is an example of using an async function in JavaScript:

const slowFunctionPromise = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('result of slow function');
    }, 1000);
  });
};

const asyncEventLoop = async () => {
  console.log('start');

  const result = await slowFunctionPromise();
  console.log(result);

  console.log('end');
};

asyncEventLoop();

Output:

start
result of slow function
end

In this example, the asyncEventLoop() function is marked as async and the slowFunctionPromise() is awaited within the function. This allows the program to wait for the Promise to resolve in a synchronous-like style, without blocking the event loop.

Async Patterns - Node's Native Option

Node.js provides several native options for handling asynchronous code. One native option in Node.js is the util.promisify() function, which can be used to convert a callback-based function to a Promise-based one.

This can be useful when working with a library or API that only provides callback-based functions, but you want to use Promises or async functions in your code.

Here is an example of using util.promisify() to convert the fs.readFile() function to a Promise-based one:

const fs = require('fs');
const util = require('util');

const readFilePromise = util.promisify(fs.readFile);

console.log('start');

readFilePromise('file.txt', 'utf8')
  .then((data) => {
    console.log(data);
  })
  .catch((error) => {
    console.error(error);
  });

console.log('end');

Output:

start
end
contents of file.txt

In this example, the readFilePromise() function is created by calling util.promisify() with the fs.readFile() function as an argument. It returns a Promise that is resolved with the contents of the file when the file has been read.

Conclusion

We can conclude that the event loop is a programming construct that enables non-blocking I/O operations in a program. It is an infinite loop that waits for events or tasks to be added to a queue and then processes them one by one.

We have seen several patterns for handling asynchronous code in JavaScript and Node.js, such as Promises, async functions, and native options. Each of these patterns has its own benefits and drawbacks, and it's important to choose the right one for your specific use case.

By understanding the event loop and using the appropriate async pattern, we can write efficient and performant code that delivers a great user experience.

References

Thank you so much for reading!πŸ™πŸ»

I hope you found the blog helpful. If you would like to stay up-to-date on future blog posts, please consider subscribing to the newsletter. I appreciate your support and look forward to sharing more content with you.

Did you find this article valuable?

Support Nikhil's Blog by becoming a sponsor. Any amount is appreciated!