brainsteam.co.uk/brainsteam/content/posts/2023/02/async-promise-constructors.md

2.5 KiB

date description mp-syndicate-to post_meta preview tags title type url
2023-02-01 09:39:03+00:00 Why you shouldn't use async in your new Promise()
https://brid.gy/publish/mastodon
https://brid.gy/publish/twitter
date
/social/2dc42dc49c5694f51e252826163f1b799e811664bd8d7defc90ce9e01af09aad.png
nodejs
typescript
javascript
softeng
Async Promise Constructors posts /2023/2/1/async-promise-constructors

I ran into an interesting typescript/js problem yesterday at work.

The following code snippet was generating an error and a stack trace that was never being propagated up to the caller:

return new Promise(async (resolve, reject) => {
  const data = await this.client.getObject(bucket, path);

  const buf: any[] = [];

  data.on("data", (data) => {
    buf.push(data);
  });

  data.on("error", (err) => {
    reject(err);
  });

  data.on("end", () => {
    resolve(JSON.parse(Buffer.concat(buf).toString()));
  });
});

The code basically uses minio's client library to get the contents of an object in an S3 storage bucket and then wraps the stream that it returns in a promise that will eventually parse the content of the S3 object into a JS object or reject if something went wrong.

Seasoned js folks may have already spotted the problem (or at least guessed it based on the blog post title).

I'ved used an async function inside my Promise constructor. I'm doing this so that I can make my async call to S3 but this has the added side effect of turning my promise constructer itself into a promise (promise-ception?). Promise constructors can't await so effectively if the getObject async call fails, the error is lost and nothing happens and the outer promise is never resolved or rejected.

As it turns out, eslint has a rule for this which I had turned off.

Solution

I could make the getObject outer function async and then do the getObject() call in the outer layer:

async function someFunction() {
  const data = await this.client.getObject(bucket, path);

  return new Promise((resolve, reject) => {
    const buf: any[] = [];

    data.on("data", (data) => {
      buf.push(data);
    });
    data.on("error", (err) => {
      reject(err);
    });
    data.on("end", () => {
      resolve(JSON.parse(Buffer.concat(buf).toString()));
    });
  });
}

Now if the getObject call fails the whole someFunction() fails (effectively the promise that is generated by use of the await/async syntactic sugar is rejected).