setTimeout is a callback-style function. What would happen if we change that?

JavaScript Joel - Oct 8 '18 - - Dev Community

Clock gold timepiece

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.

Ryo fireballing code in 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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And the callback itself must look like this:

const callback = (err, data) => { /* ... */ }
//                ---  ----
//               /          \
//    error first             data last
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
)
Enter fullscreen mode Exit fullscreen mode

A few key things I would like to point out, regarding this code.

  • sleep is curried. You'll see why later.
  • sleep takes a value and 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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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 })
Enter fullscreen mode Exit fullscreen mode

Pretty damn easy!

The text

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 })
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
])
Enter fullscreen mode Exit fullscreen mode

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

Cheers!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .