JavaScript Microtasks

JavaScript promises use the microtask queue for running callbacks. This chapter is dedicated to the exploration of microtasks in JavaScript.

Before getting to microtasks it is useful to define JavaScript tasks.

A task is considered a JavaScript code that is scheduled to be executed by standard mechanisms. For instance, beginning to run a program, an event callback that is being executed, a timeout or an interval that is being fired. They are scheduled in the queue of the task.

A microtask is a brief function that is run after the function or program that created its exits and merely when the execution stack of JavaScript is empty but before returning the control to the event loop.

As you may already know, promise handlers such as .then, .catch, and .finally are known as asynchronous.

Even once a promise is considered resolved, the code placed on the lines below these handlers still runs before them, like here:

let promise = Promise.resolve();
promise.then(() => console.log("promise done!"));
console.log("code finished"); // this show appears first

Invoking it will first show code finished and then promise done. That is not normal behavior as the promise should be done at the start. To find out what is going go further.

Microtasks Queue

It is necessary to manage asynchronous tasks properly.

Once a promise is ready, the .then/catch/finally handlers are placed in a queue but without executing. When the JavaScript engine gets free from the code, its task is now to execute it.

In case of the presence of a chain with different .then/catch/finally, each of them runs asynchronously.

If you want to urge code finished execute after promise done, you can place it into the que along with .then, like this:

Promise.resolve()
  .then(() => console.log("promise done"))
  .then(() => console.log("code finished"));

So, the order can be regarded as intended.

Unhandled Rejection

The unhandled rejection can happen in case a promise error can’t be handled at the end of the microtask queue.

As a rule, the .catch handler is added to the promise chain for handling an expected error:

let promise = Promise.reject(new Error("Promise failed"));
promise.catch(err => console.log('caught'));
// does not run: error handled
window.addEventListener('unhandledrejection', event => console.log(event.reason));

But, missing out to add the .catch will make the engine trigger the event, like here:

<!DOCTYPE html>
<html>
  <title>Title of the document</title>
  <head></head>
  <body>
    <script>
      let promise = Promise.reject(new Error("Promise failed"));
      // Promise failed
      window.addEventListener('unhandledrejection', event => alert(event.reason));
    </script>
  </body>
</html>

Also, the error can be handled later, as follows:

<!DOCTYPE html>
<html>
  <title>Title of the document</title>
  <head></head>
  <body>
    <script>
      let promise = Promise.reject(new Error("Promise failed"));
      setTimeout(() => promise.catch(err => alert('caught')), 1000);
      // Error: Promise failed
      window.addEventListener('unhandledrejection', event => alert(event.reason));
    </script>
  </body>
</html>

Running it will lead to Promise failed first and then to caught.

As we described above, unhandledrejection is created when the microtask queue is complete. The engine investigates the promises. In case of detecting a rejected one, the event occurs.

In the case, demonstrated above, the .catch handler added by setTimeoutoccurs too. But, it triggers after unhandledrejection, hence nothing is changed.

Summary

The promise handling process is always asynchronous because all the promise actions pass through the internal microtask queue.

The handlers such as .then/catch/finally should be called after the current code is over. To make sure that these handlers are executed, it is required to add them to the .then call.

The microtasks concepts in JavaScript are closely linked to the event loop macrotasks, as well.




Do you find this helpful?

Related articles