Understanding `new Promise` in JavaScript

Jan 24, 2020

The Promise constructor takes a single parameter, an executor function. JavaScript then executes your executor function with 2 arguments: resolve() and reject().

function executor(resolve, reject) {
  typeof resolve; // 'function'
  typeof reject; // 'function'
}

new Promise(executor);

Your executor function is responsible for calling resolve() to mark the promise as fulfilled (successful) or rejected (failed).

const success = new Promise(function executor(resolve) {
  resolve('OK');
});

const fail = new Promise(function executor(resolve, reject) {
  reject(new Error('Oops'));
});

const str = await success;
str; // 'OK'

const err = await fail.catch(err => err);
err.message; // Oops

You can register an event listener for when a promise is fulfilled or rejected using the then() function.

const success = new Promise(function executor(resolve) {
  setTimeout(() => resolve('OK'), 100);
});

const start = Date.now();

return success.then(function onFulfilled(str) {
  str; // 'OK'

  const elapsed = Date.now() - start;
  elapsed; // Approximately 100
});

Using Promises for Timeouts

You don't need to create new promises very often. Usually, libraries like Axios or Mongoose create promises internally and return them, so you can use then() or await.

However, not all APIs support promises. For example, the setTimeout() function only accepts callbacks. In order to create a promise that waits for 100ms before resolving, you should wrap a setTimeout() call in a new Promise:

async function test() {
  // Pause the async function for 100ms
  await new Promise(resolve => setTimeout(resolve, 100));

  return 'OK';
}

const start = Date.now();
await test();
const elapsed = Date.now() - start;
elapsed; // Approximately 100

Wrapping Node-Style Callbacks

Some async Node.js APIs, like fs.readFile(), don't return promises. You also need to wrap fs.readFile() in a new Promise in order to use it with async/await.

Make sure you handle errors! Node-style callbacks take 2 parameters: an error and a result. If error is not nullish, you should reject the promise.

const fs = require('fs');

const p = new Promise(function executor(resolve, reject) {
  fs.readFile('./package.json', (error, result) => {
    if (error != null) {
      // Note the early return!
      return reject(error);
    }

    resolve(result);
  });
});

const pkg = JSON.parse(await p);
pkg.name; // 'masteringjs.io'

Async Executor Functions

A common mistake is making the executor an async function.

const p = new Promise(async function executor(resolve, reject) {
  await new Promise(resolve => setTimeout(resolve, 100));
  resolve('OK');
});

const str = await p;

The above code works fine, but it creates an unnecessary promise (remember that async functions always return a promise!) and looks clumsy. Since async functions always return promises, you can always replace an async executor function with a vanilla async function call:

async function test() {
  await new Promise(resolve => setTimeout(resolve, 100));
  return 'OK';
}

const p = test();

The key takeaway is that you should never make an executor function async. There's no need.


More Fundamentals Tutorials