Promise.withResolvers in JavaScript

Let me know if you've heard this one before. You have a function that is reading some sort of file (let's say a csv with a known format and no header line). You want to use streams so you can act on each line but you need to return a promise that resolves once the file has been fully read. Nine times out of ten, you've probably been wrapping the whole function up in a promise definition. I'd suspect something like the below code.

// The data type we want from our csv
type Child = {
  name: string,
  mother: string,
  father: string
}

function readCsv(filePath: string): Promise<Child[]> {
  // Return a new Promise so that this method can be used easily in our wider async program
  return new Promise((resolve, reject) => {
    // Create the array that we'll be returning
    let response: Child[] = [];
    
    // Create our readline instance
    const rl = readline.createInterface({
        input: fs.createReadStream(filePath),
        crlfDelay: Infinity,
    });
    
    // Every time we read a line, add it to the array
    rl.on('line', (line) => {
      let [name, mother, father] = line.split(",");
      response.push({ name, mother, father });
    });
    
    // Resolve our promise with our completed array once the file has been completed
    rl.on('close', () => resolve(response));
  });
}

Perhaps, you try to be a bit cleaner with your code and have instead been pulling the resolve and reject methods out of the promise to avoid unnecessary indentation. A worthy goal to make the code "cleaner" but at the cost of adding a bit of additional boilerplate to the start of your function.

function readCsv(filePath: string): Promise<Child[]> {
  // Declare resolve and reject outside of promise scope
  let resolve;
  let reject;
  // Create a new promise
  let promise = new Promise((resolve_, reject_) => {
    // Pull the resolve_ and reject_ methods out of the promise scope by assigning to the resolve and reject variables
		resolve = resolve_;
    reject = reject_;
  });
  
  // Create the array that we'll be returning
  let response: Child[] = [];

  // Create our line reader
  const rl = readline.createInterface({
      input: fs.createReadStream(filePath),
      crlfDelay: Infinity,
  });

  // Every time we read a line, add it to the array
  rl.on('line', (line) => {
    let [name, mother, father] = line.split(",");
    response.push({ name, mother, father });
  });

  // Resolve our promise with our completed array once the file has been completed
  rl.on('close', () => resolve(response));
  
  // return our promise
  return promise;
}

Enter Promise.withResolvers

As of this month (March 2024) you actually have a new option, using Promise.withResolvers, which returns an object containing a new promise as well as the resolve and reject methods. This means that with a bit of destructuring, you can pull out your resolvers in a single clean line.

let { promise, resolve, reject } = Promise.withResolvers();

It's already supported in all major browsers as well as in NodeJS behind a feature flag. As of Node v21.7.1, using node --js-promise-withresolvers index.js will enable the flag for your script. That being said, I'd imagine NodeJS will support it fully within a short period of time.

Let's take another look at our CSV reading example. This time though, we'll use the withResolvers method rather than manually pulling out the resolvers ourselves.

function readCsv(filePath: string): Promise<Child[]> {
  // Declare our promise, resolve and reject using Promise.withResolvers
  let { promise, resolve, reject } = Promise.withResolvers();
  
  // Create the array in this scope so it can be accessed in lower scopes
  let response: Child[] = [];

  // Create our readline instance
  const rl = readline.createInterface({
      input: fs.createReadStream(filePath),
      crlfDelay: Infinity,
  });

  // Every time we read a line, add it to the array
  rl.on('line', (line) => {
    let [name, mother, father] = line.split(",");
    response.push({ name, mother, father });
  });

  // Resolve our promise with our completed array once the file has been completed
  rl.on('close', () => resolve(response));
  
  // return our promise
  return promise;
}

Some Other Example Use Cases

A regular use case for manually creating promises is for an awaitable wait function. Despite both versions of this code taking up 5 lines, I think you'll agree with me that the withResolvers version is much quicker to parse mentally.

// Without Promise.withResolvers
function wait(time: number) {
  return new Promise((resolve, reject) => {
    setTimeout(time, resolve);
  })
}

// With Promise.withResolvers
function wait(time: number) {
  let { promise, resolve } = Promise.withResolvers();
  setTimeout(time, resolve); // EDIT: 01/04/2024 - Apparently I typoed this as setTimeout(time, reso🥧lve) and noone noticed
  return promise;
}

Because you can pass around the resolvers from Promise.withResolvers, it allows you to do some interesting things in reactive frameworks like Svelte or React. The following code uses the resolve and reject methods to log out a message only after a button has been pressed by abusing standard .then and .catch handlers.

<script lang="ts">
  // lets get our promise and resolvers
	let { promise, resolve, reject } = Promise.withResolvers();
  // Because the promise won't resolve until a button has been pressed, we can dump our resolution handling here
  promise
    .then((emoji) => console.log(`I really like ${emoji}`))
    .catch((emoji) => console.log(`I don't like ${emoji}`));
</script>
<!-- Once one of these buttons is pressed, the Promise will either resolve or reject trigering our resolution handlers -->
<button on:click={() => resolve("🥧")}>I like Pie</button>
<button on:click={() => reject("🥧")}>I hate Pie</button>

Conclusion

I'm really excited to see all the ways this simple addition to promises impacts our coding practices. If you end up using Promise.withResolvers for anything interesting, please leave me a comment over on Github or Bluesky to tell me about it.