setTimeout is a callback-style function. What would happen if we change that?
joelnet
Oct 8
Updated on Oct 09, 2018
Today it is common practice to transform node-style-callback functions into promise-style functions. So why haven't we done this for
setTimeout?
The main reason to prefer a promise-style function over a node-style-callback is to avoid Callback Hell.
Nobody wants to see that.
After looking at
setTimeout (and it's siblings
setInterval or
setImmediate), I can clearly see that it's a callback-style function.
setTimeout(callback, 1000); // -------- // \ // See that? Right there. A callback!
Yet, it's so incredibly rare to see anyone convert
setTimeout from callback to a promise. How has
setTimeout flown under the radar? Is
setTimeout different enough to get a pass?
I say no.
Node-style-callback functions
setTimeout may have been passed over because even though It's clearly a callback-style function, it is not a node-style-callback function, which is a little different.
First, let's have a look at node-style-callbacks to better see the difference.
fs.readFile is a great example of a node-style-callback function.
fs.readFile(path[, options], callback) // -------- // / // callback must be last
And the callback itself must look like this:
const callback = (err, data) => { /* ... */ } // --- ---- // / \ // error first data last
If
setTimeout was a traditional node-style-callback function, it could be easily converted with node's
util.promisify. Here's an example of how easy it is to use
util.promisify to convert
fs.readFile into a promise-style function.
import fs from 'fs' import { promisify } from 'util' const readFile = promisify(fs.readFile)
Unfortunately,
util.promisify will not work. First, because the callback is not the last argument. Second, because the callback does not follow the
(err, data) interface.
Promisifying setTimeout
Fortunately, transforming this manually is just as simple. I'll call the new function
sleep.
const sleep = milliseconds => value => new Promise (resolve => setTimeout(() => resolve(value), milliseconds) )
A few key things I would like to point out, regarding this code.
sleepis curried. You'll see why later.
sleeptakes a
valueand then resolves the
value. Again, you'll see why later.
Using sleep
Adding a pause into your code is now as simple as using a promise.
const log => msg => console.log(msg) sleep(1000)('Hello World').then(log)
That's fine, but not the reason why I am writing this.
What really excites me about
sleep is the ability to slip it into the middle of promise chains.
In this example, it was trivial to add a 1 second delay between API calls.
import axios from 'axios' import sleep from 'mojiscript/threading/sleep' const fetchJson = url => axios.get(url).then(response => response.data) const log = msg => (console.log(msg), msg) // - // / // comma operator. google it. fetchJson('') .then(log) .then(sleep(1000)) .then(() => fetchJson('')) .then(log) .then(sleep(1000)) .then(() => fetchJson('')) .then(log)
Because
sleep takes a value as input and then returns the same value, it will pass the value through to the next promise.
sleep basically becomes Promise chain middleware.
Let's see this written in async/await style:
import axios from 'axios' import sleep from 'mojiscript/threading/sleep' const fetchJson = url => axios.get(url).then(response => response.data) const log = msg => (console.log(msg), msg) const main = async () => { const people1 = await fetchJson('') log(people1) await sleep(1000) const people2 = await fetchJson('') log(people2) await sleep(1000) const people3 = await fetchJson('') log(people3) } main()
Now to be honest, I like the problem
sleep solves, but I'm not quite in love with the syntax of either of those codes I just demonstrated. Between these two examples, I actually think the
async/await syntax is the worse.
await is sprinkled all over the place and it's easy too easy to make a mistake.
Asynchronous Function Composition
Function composition is powerful and will probably take reading many articles to fully understand. Not just the how, but the why. If you want to start, I would recommend starting here: Functional JavaScript: Function Composition For Every Day Use .
I'm intentionally not explaining function composition in this article. I believe the syntax I am about to show you is so simple that you do not need to understand function composition at all.
import axios from 'axios' import pipe from 'mojiscript/core/pipe' import sleep from 'mojiscript/threading/sleep' const fetchJson = url => axios.get(url).then(response => response.data) const log = msg => (console.log(msg), msg) const main = pipe ([ () => fetchJson(''), log, sleep(1000), () => fetchJson(''), log, sleep(1000), () => fetchJson(''), log ]) main()
Damn. That is some good looking code!
But since we're already talking about function composition, it would be easy to extract
fetchJson,
log,
sleep into it's own
pipe and make the code a little more DRY.
import axios from 'axios' import pipe from 'mojiscript/core/pipe' import sleep from 'mojiscript/threading/sleep' const fetchJson = url => axios.get(url).then(response => response.data) const log = msg => (console.log(msg), msg) const fetchLogWait = pipe ([ id => fetchJson (`{id}`), log, sleep(1000) ]) const main = pipe ([ () => fetchLogWait (1), () => fetchLogWait (2), () => fetchLogWait (3) ]) main()
Asynchronous map
MojiScript also has the unique ability to asynchronously map. (Expect an entire article on this in the near future).
Async map is why I decided to write these examples using MojiScript's
pipe instead of Ramda's
pipeP. Up to this point, the examples will also work just fine with Ramda's
pipeP. From this point on, the examples are MojiScript exclusive.
Let's see some code! How easy it is to asynchronously
map the ajax calls?
const main = pipe ([ ({ start, end }) => range (start) (end + 1), map (fetchLogWait), ]) main ({ start: 1, end: 3 })
Pretty damn easy!
All together in one runnable code block:
import axios from 'axios' import log from 'mojiscript/console/log' import pipe from 'mojiscript/core/pipe' import map from 'mojiscript/list/map' import range from 'mojiscript/list/range' import sleep from 'mojiscript/threading/sleep' const fetchJson = pipe ([ axios.get, response => response.data ]) const fetchLogWait = pipe ([ id => fetchJson (`{id}`), log, sleep (1000) ]) const main = pipe ([ ({ start, end }) => range (start) (end + 1), map(fetchLogWait), ]) main ({ start: 1, end: 3 })
Now this code is about as DRY as it gets!
setTimeout in a for loop
Now if you haven't seen this problem yet, it's given during a lot of JavaScript interviews. The code doesn't run as expected. What is the output?
for (var i = 1; i < 6; i++) { setTimeout(() => console.log(i), 1000) }
If you didn't guess it pauses for 1 second and then prints five
6's all at once, then you would be wrong.
The same program written using
pipe and MojiScript's
map. Except this one works as expected, printing the numbers 1 through 5 with a 1 second pause before each output.
const sleepThenLog = pipe ([ sleep (1000), log ]) const main = pipe ([ range (1) (6), map (sleepThenLog) ])
Want to play more? Getting started with MojiScript: FizzBuzz
Things to google
Summary
Converting sleep into a promise-style function provides additional options to how async code is run.
Ramda's
pipeP or MojiScript's
pipe can sometimes be cleaner than
Promises or
async/await.
Asynchronous map is powerful.
One caveat, pointed out below, is this implementation does not allow for cancellation. So if you need to
clearTimeout, you will need to modify this function.
My articles are very Functional JavaScript heavy, if you need more, follow me here, or on Twitter @joelnet!
Read my other articles:
Why async code is so damn confusing (and a how to make it easy)
How I rediscovered my love for JavaScript after throwing 90% of it in the trash
What little things make you happy while coding?
I must say refactoring code to make it look beautiful is my guilty pleasure ...
Ok, that's so cool but... what about
clearTimeout?
Because I'm sure many have thought about transforming
setTimeoutinto a promise (e.g. was common in AngularJS), but there's always a catch.
There has been a debate about cancelable promises a couple of years ago, we got nothing it out of it 😕
This is a fantastic question. Without cancellable promises, you would not be able to cancel the
setTimeout.
For a majority of use cases cancelling a promise is not needed. Though that doesn't mean it is not a valid use case. Throttling and debouncing is an example of a valid use case when you would want to cancel a promise.
The
sleepimplementation I created currently does not support cancellation. I have created an issue for this here: github.com/joelnet/MojiScript/issu.... This is a feature that would be great to have.
Here's a very naive implementation that could support cancellation:
Bluebird also supports promise cancellation: bluebirdjs.com/docs/api/cancellati...
Converting this
Promiseto use bluebird's
Promisewould also work.
Yes, that could work! 🙌
You can also consider the approach that's been taken for cancelling a
fetchpromise: it's based on a "signal" that's being created by an
AbortControllerobject:
You can extend your
sleepfunction to accept a
signaloption and listen to an
abortevent. Although it does seem a little overkill for something like this...
This is interesting. I'll have to create some test project with the
.canceland an
AbortController. Maybe I'll create a
debounceto compare the differences.
Definitely something to think about.