setTimeout is a callback-style function. What would happen if we change that?
JavaScript Joel
Posted on October 8, 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.
-
sleep
is curried. You'll see why later. -
sleep
takes avalue
and then resolves thevalue
. 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('https://swapi.co/api/people/1')
.then(log)
.then(sleep(1000))
.then(() => fetchJson('https://swapi.co/api/people/2'))
.then(log)
.then(sleep(1000))
.then(() => fetchJson('https://swapi.co/api/people/3'))
.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('https://swapi.co/api/people/1')
log(people1)
await sleep(1000)
const people2 = await fetchJson('https://swapi.co/api/people/2')
log(people2)
await sleep(1000)
const people3 = await fetchJson('https://swapi.co/api/people/3')
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('https://swapi.co/api/people/1'),
log,
sleep(1000),
() => fetchJson('https://swapi.co/api/people/2'),
log,
sleep(1000),
() => fetchJson('https://swapi.co/api/people/3'),
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 (`https://swapi.co/api/people/${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 (`https://swapi.co/api/people/${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
Posted on October 8, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 8, 2018