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;
};
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;
};
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;
};
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;
};
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
)
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);
}
})
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
})
And, what's even better, you can also do this.
const result = await promise;
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;
}
Then you can call your new function with await
.
const image = await drawImage('data');
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));
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)
You might want to add the "once" option to those eventListeners!
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.
Nicely done!
Also, in nodejs, if you want to make some old callback-based functions into promises, just import promisify from util and bam!
Neat 🙂🙂. 💪💪💪
Helpful, thanks.