If you've ever wanted to await
JavaScript's setTimeout
or setInterval
functions, you're not alone. I've had to use these methods a lot at work to deal with some interesting third party behavior, so I've finally become familiar with promisifying functions. setTimeout is simple, but setInterval is a tad trickier, so make sure you understand promises before diving in here.
TL;DR: The Code
If you want to see where we're heading, here are the two utility functions. Read on for explanations, or take this and be on your way!
How to promisify setTimeout
Take this code:
The annoying thing about setTimeout
is that everything that you want to happen after the delay has to go inside that callback function. It can get cumbersome if there's a lot to do, or any additional timers. But there's no callback needed when we use await
:
What's happening here? Well, as you know a Promise really just calls a resolve
function or reject
function after something happens. What we're doing here is creating a new Promise
and instead of passing in some callback function to setTimeout
, we're passing in resolve
. That way, after the milliseconds pass, it is resolve
that gets invoked, and our promise is triggered and resolved. We can then await
it and do whatever we want. Easy peasy...but it can be even peas-ier.
Making a sleep function
People tend to want to use the setTimeout
function like a pause button. Other languages have a sleep
function to halt their program for a set amount of time. Let's make one!
This is a simple utility function that can really come in handy, especially if it's used it more than once, since nesting setTimeout
s looks truly terrible.
Promisifying setInterval
If you want setInterval
to run forever, you're in a pickle. You can't promisify that. But if you just want to use it to handle a few retries and then move on, you can make that work. To do it, we're going to use the same resolve
trick as before, but we'll also add a max amount of tries. If we don't get something by a certain point, we'll move on and use reject
. Now, we will still have to pass in a callback function this time. That's because unlike sleep, which always happens, we now have to know if we've succeeded or not.
The use case I'm specifically thinking of is checking the DOM for certain elements. It's unfortunate, but sometimes you'll be integrating with a third party widget that doesn't give you any "ready" hooks. This will force you to check the existence of an element in the DOM manually over and over again.
The trick here is that our passed in callback
must only return a truthy value if it succeeds. That way, we can know whether or not to move on. This could be modified further if you wanted the callback itself to be asynchronous, but be careful with race conditions (it's possible the task could take longer than the interval itself).
No awards for looks
And I know it's a bit ugly with all those nested functions but we have to do that in order to form a closure with access to all our parameters
and the resolve
and reject
functions from the new Promise
. What I found helpful for readability is not defining another callback inline in setInterval
, but rather as it's own expression above (though still nested in the Promise
). The alternative to nesting is currying, but that feels like the cure is worse than the disease in this case.
The default values for ms
and triesLeft
are up to you as well, I just found these to be handy for my usecases. You also don't have to resolve
with the actual value, but I've found it's helpful. Especially when dealing with the DOM, it keeps us from having to make another query. reject
should always pass some kind of error, but change the message to one that makes more sense for your code.
Make sure to use clearInterval
, otherwise this code will run forever!
Promises all the way down
There you go! With these two functions, sleep
and asyncInterval
, you don't have the awkward mix of timed callbacks and regular promises. I know using these kinds of timers isn't super common, but it's always good to have these techniques in your toolbox.
Happy coding everyone,
Mike