DEV Community

Cover image for A real-life example of making a custom Promise in JavaScript/TypeSctipt
NordicBeaver
NordicBeaver

Posted on • Edited on

A real-life example of making a custom Promise in JavaScript/TypeSctipt

It feels like we've completely moved from callbacks to Promises and async/await in the JavaScript world. So much, that almost every library and framework provides async versions of their functions. And the logic usually goes like this: 'I see async -> I type await and mark my function async as well -> done!'. I got so used to it, so I started to forget how to create and use my own Promises.

But life always finds a way to remind you that old habits die hard, especially if you're talking about browsers APIs.

So, if you're like me and need a little reminder on how to make custom Promises, I hope you'll find this post helpful.

Creating html images.

So here is the problem. We need to create an Image element using a browser's API and set its source to a dataUrl of some picture. At first, I wrote something like this.

function drawImage(dataUrl: string) {
  const image = new Image();
  image.src = dataUrl;
  return image;
};
Enter fullscreen mode Exit fullscreen mode

Looks great, but there is a problem here. When we set image.src it's not instantly loaded. Even if a source is a data string, and not an URL to an external resource, it still takes time. So when an image is returned from the function, the is no guarantee that the data is already there.

Unfortunately, if you want to wait for the loading to be done, you can't do something like this.

function drawImage(dataUrl: string) {
  const image = new Image();
  await image.src = dataUrl; // STOP! THIS IS ILLEGAL!
  return image;
};
Enter fullscreen mode Exit fullscreen mode

The only way is to set an event handler. But what do we put here?

function drawImage(dataUrl: string) {
  const image = new Image();
  image.addEventListener('load', () => {
    // ???
  });
  image.src = dataUrl;
  return image;
};
Enter fullscreen mode Exit fullscreen mode

If we were still in 2010, we would solve this by having another argument for a callback function.

function drawImage(dataUrl: string, onDone: () => void) {
  const image = new Image();
  image.addEventListener('load', () => {
    onDone();
  });
  return image;
};
Enter fullscreen mode Exit fullscreen mode

But wait, it's 2021. Cool kids don't do that anymore. What we need, is to make our function return something awaitable. And there is nothing more awaitable than a Promise. Here is the Promise constructor

function Promise<T>(
  executor: (
    resolve: (value: T) => void, 
    reject: (reason?: any) => void
  ) => void
)
Enter fullscreen mode Exit fullscreen mode

Looks a bit scary, but you just need to get to know it better. This is how you usually call it.

const promise = new Promise((resolve, reject) => {
  // Lalala, do some work
  // ....
  if (we_are_good_and_got_the_result) {
    resolve(result);
  } else {
    reject(error);
  }
})
Enter fullscreen mode Exit fullscreen mode

So you pass a function in the constructor. A function that has 2 arguments, which are also functions. Call them resolve and reject. In that function we do some work. If everything went well, just call resolve with the result. Otherwise call reject with an error object.
The thing that you passed into the resolve is gonna be returned as a Promise result when it resolves.

Then you can use this new object.

promise.then((result) => {
  // Use the result
}).catch(error => {
  // Oh no there was an error
})
Enter fullscreen mode Exit fullscreen mode

And, what's even better, you can also do this.

const result = await promise;
Enter fullscreen mode Exit fullscreen mode

Let's apply what we learned to our problem.

function drawImage(dataUrl: string) {
  const promise = new Promise<HTMLImageElement>((resolve, reject) => {
    const image = new Image();
    image.addEventListener('load', () => {
      resolve(image);
    }, { once: true }); // We don't want this handler to run more than once.
    image.src = dataUrl;
  });
  return promise;
}
Enter fullscreen mode Exit fullscreen mode

Then you can call your new function with await.

const image = await drawImage('data');
Enter fullscreen mode Exit fullscreen mode

And that's it. We've done it!

Bonus example for those who can't wait.

The is one more example I want to show you. It's actually quite useful to be able to just wait for a set amount of time. Sadly, there is not wait() or sleep() function in JavaScript. But with our new powers we can make our one. Here is a quick one-liner for you guys to try and figure out as an exercise (I feel like a college teacher by saying this lol)

const wait = async (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
Enter fullscreen mode Exit fullscreen mode

Show me more please Senpai.

This article is actually an extended version of an excerpt of my latest post. You're welcome to check it out if you want more ;).

Top comments (6)

Collapse
 
gustvandewal profile image
Gust van de Wal

You might want to add the "once" option to those eventListeners!

Collapse
 
nordicbeaver profile image
NordicBeaver

You're right! Thank you for noticing that.
I don't think it was an issue in my case, but I guess it can cause weird bugs in some scenarios. And it totally feels more correct, we never need that event handler to be executed more that once.
I'll update the article.

Collapse
 
biiishal profile image
bishal

Nicely done!

Collapse
 
bherbruck profile image
bherbruck

Also, in nodejs, if you want to make some old callback-based functions into promises, just import promisify from util and bam!

Collapse
 
therealokoro profile image
Okoro Redemption

Neat 🙂🙂. 💪💪💪

Collapse
 
viperson95 profile image
ViPerson95

Helpful, thanks.