JavaScript Promises: The Definitive Guide, Part 1

JavaScript Promises: The Definitive Guide, Part 1

If you deal with Promises in Javascript, then you absolutely need to read this guide.

The single-threaded, event-loop based concurrency model of JavaScript deals with processing of events using so-called “asynchronous non-blocking I/O model.” Unlike computer languages such as Java, where events are handled using additional threads and processed in parallel with the main execution thread, JavaScript code is executed sequentially. In order to prevent blocking the main thread on I/O-bound operations, JavaScript uses a callback mechanism where asynchronous operations specify a callback – the function to be executed when the result of an asynchronous operation is ready; while the code control flow continues executing.

Whenever we want to use the result of a callback to make another asynchronous call, we need to nest callbacks. Since I/O operations can result in errors, we need to handle errors for each callback before processing the success result. This necessity to do error handling and having to embed callbacks makes the callback code difficult to read. Sometimes this is referred to as “JavaScript callback hell.”

In order to address this problem, JavaScript offers a mechanism called a Promise. It is a common programming paradigm (more about it here: https://en.wikipedia.org/wiki/Futures_and_promises) and TC39 introduced it in ECMAScript 2015. The JavaScript Promise is an object holding a state, which represents an eventual completion (or failure) of an asynchronous operation and its resulting value.

A new Promise is in the pending state. If a Promise succeeds it is put in a resolved state otherwise it is rejected. Instead of using the original callback mechanism, code using Promises creates a Promise object. We use Promises typically with two callback handlers – resolved invoked when the operation was successful and rejected called whenever an error has occurred.

// Converting a callback based method to a method that returns Promise
const fs = require('fs')

const readTextFromFile = new Promise((resolve, reject) => {
  fs.readFile('file.txt', (err, data) => {
    if (err) {
      return reject(err)
    }

    resolve(data)
  })
})

// Usage of a method that returns Promise
readTextFromFile()
  .then(data => console.log(data))
  .catch(e => console.log(e))

Process.nextTick(callback)

To understand how Promises work in Node.js, it is important to review how process.nextTick() works in Node.js, as the two are very similar. Process.nextTick() is a method that adds a callback to the “next tick queue.” Tasks in the queue are executed after the current operation in the event loop is done and before the event loop is allowed to continue. Simply said, there’s another queue beside the event loop that we can use to schedule events. This queue is even faster than the event loop and it may be drained several times in a single event loop tick.

const log = msg => () => console.log(`NEXT TICK ${msg}`)

const timeout = (time, msg) => {
  setTimeout(() => {
    console.log(`TIMEOUT ${msg}`)
  }, time)
}

process.nextTick(log('ONE'))
timeout(0, 'AFTER-ONE')
process.nextTick(log('TWO'))
timeout(0, 'AFTER-TWO')

In the example above, we can see how process.nextTick works in practice. We have two setTimeout calls, with callbacks immediately scheduled in the event loop. We also have two process.nextTick methods with callbacks scheduled in the “next tick queue.” This is what we see in the console:

Next TICK ONE
Next TICK TWO
TIMEOUT AFTER-ONE
TIMEOUT AFTER-TWO

Since we know that “next tick queue” is separate from event loop and can be drained multiple times in a single event loop tick, this makes sense. Two nextTick callbacks are executed immediately and the other two setTimeout callbacks, set in the event loop, are executed after.

Putting so many callbacks in the “next tick queue” may block the event loop and prevent any I/O operation. That’s why we have process.maxTickDepth that represents the maximum number of callbacks in the queue that can be executed before allowing the event loop to continue. Its default value is 1000.

How Do Promises Work?

Promises are a new and nice way to handle async code, but how do they really work? For understanding the benefits and the performance characteristics of Promises we need to understand how they are implemented and what really happens when we return new Promise() .

Promises use the Microtask queue and they are executed independently from regular tasks (the setTimeout callback, for example). What does this really mean? In JavaScript, we have three queues: (1) event loop, (2) nextTick queue and (3) Microtask queue. All those queues work independently.

Macrotasks are regular tasks that are going into the event loop and in one event loop tick, only one Macrotask is executed. Microtasks have an independent queue and, in one event-loop tick, the whole microtasks queue can be drained. This gives us a really good performance benefit. Basically, we use microtasks when we need to do stuff asynchronously in a synchronous way, as fast as possible.

Promises are executed as Microtasks. This means that they are executed sooner than Macrotasks. They are never executed concurrently. Microtasks are always executed sequentially, so talking about parallelism with Promises is wrong. They work like process.nextTick, independently from event loop in their own microtask queue.

Macrotasks: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering

Microtasks: process.nextTick, Promises, Object.observe, MutationObserver (read more here)

const fetch = require('node-fetch') // only when running in Node.js

const fetchData = fetch('https://api.github.com/users/nearform/repos')
  .then(() => console.log('Hi from fetch!'))
  .catch(e => console.error(e))

console.log('Hi!')

setTimeout(() => {
  console.log('Hi from setTimeout')
}, 0)

fetchData()

In the example above, the code in the Promise will be scheduled in the Microtask queue, but since that action requires the network, it will only be resolved after the data is received. In this example, we’ll see this output:

Hi!
Hi from setTimeout!
Hi from fetch

We also need to mention that the timing of callbacks and Promises can vary significantly depending on the environment (browser or Node.js).

Promise Methods

Promise.all(iterable)

It takes an array of Promises and returns a Promise that either fulfills when all of the Promises in the iterable argument have been fulfilled or rejects as soon as one of the Promises rejects. If the returned Promise fulfills, it’s fulfilled with an array of the values from the fulfilled Promises in the same order as defined in the array argument. If the returned Promise rejects, it is rejected with the reason from the first Promise in the array that got rejected. This method can be useful for aggregating results of multiple Promises.

The biggest confusion about Promise.all is that Promises passed in the iterable are executed concurrently. Promise.all doesn’t provide parallelism! The function passed in the Promise constructor is executed immediately and Promise is resolved in the microtask queue. Microtasks are always executed in sequence.

This method is useful when we want to wait for multiple Promises to resolve (or reject) without manually chaining them. The most common use case is mapping through an array and returning a Promise for every element:

const results = await Promise.all(

 items.map(item => generateResultFromItem(item))

)

The first rejection of a Promise will cause Promise.all() to reject, but other constituent Promises will still be executing. This can be harmful as we will be using resources for generating results that won’t be used.

const util = require('util')
const sleep = util.promisify(setTimeout)

Promise.all([
  sleep(1000).then(() => console.log('b')),
  Promise.reject('a')
]).catch((err) => console.log(err))

In the example above, we’re passing two Promises in Promise.all(). The first one is waiting one second and then logging letter b in the console. The second one is rejected with the letter a. Since the second one is rejected, we would expect to see only a in the console, but you’ll see and b. That’s because you can’t cancel the Promise. Every scheduled Promise will be executed and Promise.all just helps us to ignore the result if one of the Promises in the iterable is rejected, and gives us a rejected Promise as a result.

Promise.race(iterable)

It takes an array of Promises and executes them in the same way as Promise.all, the difference being it returns a Promise that fulfills or rejects as soon as one of the Promises in the iterable fulfills or rejects, with the value or reason from that Promise. As an example, Promise.race can be used for building a timeout functionality, where the first Promise will be an HTTP request to some service, and a second one will be a timeout function. If the second one fails first, the resulting Promise from Promise.race() will be rejected and the data from the first Promise won’t be available. The rejection of one Promise from the iterable won’t cancel others, they will be still be executed, as in the Promise.all method case.

const fetch = require('node-fetch') // only when running in Node.js

const getUserRepos = () =>
 fetch('https://api.github.com/users/nearform/repos')

const timeout = delay =>
  new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('request timeout')), delay)
  })

Promise.race([getUserRepos(), timeout(300)])
  .then(repos => console.log(repos))
  .catch(e => console.error(e))

Promise.reject(reason)

Returns a Promise object that is rejected with the given reason as an argument. It is mainly used to throw an error in the Promise chain.

Promise.resolve(value)

Returns a Promise that is resolved with the given value as an argument. It is mainly used to cast a value into the Promise, some object or array, so we can chain it later with other async code.

Async/Await

Async/await semantics were added in ECMAScript 2017, allowing programmers to deal with Promises in a more intuitive way. The word “async” before a function means one simple thing: a function always returns a Promise. If the code has returned in it, then JavaScript automatically wraps it into a resolved Promise with that value. The keyword await, which can only occur inside an async function, makes JavaScript wait until the Promise has been settled and returns its result.

Below is a function waiting on a Promise that is resolved after one second using async/await keywords:

let promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve("done!"), 1000)
})

async function f() {
  let result = await promise // wait till the Promise resolves
  alert(result) // "done!"
}

That’s all for Part 1! Tune in on Monday when we’ll discuss common mistakes with promises.

Originally published by Ivan Jovanovic at https://dzone.com

Learn more

☞ The Complete JavaScript Course 2019: Build Real Projects!

☞ JavaScript in Action - bird flying game fun with the DOM

☞ JavaScript Car Driving Game from scratch with source code

☞ Advanced JavaScript Concepts

☞ Selenium WebDriver - JavaScript nodeJS webdriver IO & more!

☞ Complete JavaScript Course For Beginners to Master - 2019

☞ The Modern JavaScript Bootcamp (2019)

Angular 9 Tutorial: Learn to Build a CRUD Angular App Quickly

What's new in Bootstrap 5 and when Bootstrap 5 release date?

Brave, Chrome, Firefox, Opera or Edge: Which is Better and Faster?

How to Build Progressive Web Apps (PWA) using Angular 9

What is new features in Javascript ES2020 ECMAScript 2020

JavaScript Tutorial: if-else Statement in JavaScript

This JavaScript tutorial is a step by step guide on JavaScript If Else Statements. Learn how to use If Else in javascript and also JavaScript If Else Statements. if-else Statement in JavaScript. JavaScript's conditional statements: if; if-else; nested-if; if-else-if. These statements allow you to control the flow of your program's execution based upon conditions known only during run time.

How to Retrieve full Profile of LinkedIn User using Javascript

I am trying to retrieve the full profile (especially job history and educational qualifications) of a linkedin user via the Javascript (Fetch LinkedIn Data Using JavaScript)

Java vs. JavaScript: Know The Difference

Java vs. JavaScript: Know the Difference, Java vs. JavaScript: What's the Difference? Java vs. JavaScript: Major Similarities and Differences. pros and cons of JavaScript and Java.